Pertanyaan ini sedikit lebih rumit daripada yang diperkirakan karena beberapa hal yang tidak diketahui: Perilaku sumber daya yang dikumpulkan, umur objek yang diharapkan / diperlukan, alasan sebenarnya bahwa kolam diperlukan, dll. Biasanya kolam adalah tujuan khusus - utas pool, pool koneksi, dll. - karena lebih mudah untuk mengoptimalkannya ketika Anda tahu persis apa yang sumber daya lakukan dan yang lebih penting memiliki kontrol atas bagaimana sumber daya itu diimplementasikan.
Karena itu tidak sesederhana itu, apa yang saya coba lakukan adalah menawarkan pendekatan yang cukup fleksibel yang dapat Anda coba dan lihat apa yang paling berhasil. Permintaan maaf di muka untuk posting lama, tetapi ada banyak alasan untuk membahas penerapan sumber daya tujuan umum yang layak. dan aku benar-benar hanya menggaruk permukaan.
Kumpulan tujuan umum harus memiliki beberapa "pengaturan" utama, termasuk:
- Strategi pemuatan sumber daya - bersemangat atau malas;
- Mekanisme pemuatan sumber daya - bagaimana cara membangunnya;
- Strategi akses - Anda menyebutkan "round robin" yang tidak semudah kedengarannya; implementasi ini dapat menggunakan buffer lingkaran yang serupa , tetapi tidak sempurna, karena kumpulan tidak memiliki kendali atas kapan sumber daya benar-benar direklamasi. Opsi lainnya adalah FIFO dan LIFO; FIFO akan memiliki lebih banyak pola akses acak, tetapi LIFO membuatnya secara signifikan lebih mudah untuk menerapkan strategi pembebasan yang paling jarang digunakan (yang Anda katakan berada di luar jangkauan, tetapi masih layak disebutkan).
Untuk mekanisme pemuatan sumber daya, .NET sudah memberi kami abstraksi bersih - delegasi.
private Func<Pool<T>, T> factory;
Lewati ini melalui konstruktor kolam dan kita hampir selesai dengan itu. Menggunakan tipe generik dengan anew()
kendala juga berfungsi, tetapi ini lebih fleksibel.
Dari dua parameter lainnya, strategi akses adalah binatang yang lebih rumit, jadi pendekatan saya adalah menggunakan pendekatan berbasis warisan (antarmuka):
public class Pool<T> : IDisposable
{
// Other code - we'll come back to this
interface IItemStore
{
T Fetch();
void Store(T item);
int Count { get; }
}
}
Konsep di sini sederhana - kami akan membiarkan publik Pool
kelas menangani masalah umum seperti keamanan utas, tetapi menggunakan "toko barang" yang berbeda untuk setiap pola akses. LIFO mudah diwakili oleh stack, FIFO adalah antrian, dan saya telah menggunakan implementasi buffer bundar yang tidak terlalu dioptimalkan tetapi menggunakan List<T>
pointer dan indeks pointer untuk memperkirakan pola akses round-robin.
Semua kelas di bawah ini adalah kelas dalam dari Pool<T>
- ini adalah pilihan gaya, tetapi karena ini benar-benar tidak dimaksudkan untuk digunakan di luar Pool
, itu paling masuk akal.
class QueueStore : Queue<T>, IItemStore
{
public QueueStore(int capacity) : base(capacity)
{
}
public T Fetch()
{
return Dequeue();
}
public void Store(T item)
{
Enqueue(item);
}
}
class StackStore : Stack<T>, IItemStore
{
public StackStore(int capacity) : base(capacity)
{
}
public T Fetch()
{
return Pop();
}
public void Store(T item)
{
Push(item);
}
}
Ini adalah yang jelas - tumpukan dan antrian. Saya tidak berpikir mereka benar-benar membutuhkan banyak penjelasan. Buffer bundar sedikit lebih rumit:
class CircularStore : IItemStore
{
private List<Slot> slots;
private int freeSlotCount;
private int position = -1;
public CircularStore(int capacity)
{
slots = new List<Slot>(capacity);
}
public T Fetch()
{
if (Count == 0)
throw new InvalidOperationException("The buffer is empty.");
int startPosition = position;
do
{
Advance();
Slot slot = slots[position];
if (!slot.IsInUse)
{
slot.IsInUse = true;
--freeSlotCount;
return slot.Item;
}
} while (startPosition != position);
throw new InvalidOperationException("No free slots.");
}
public void Store(T item)
{
Slot slot = slots.Find(s => object.Equals(s.Item, item));
if (slot == null)
{
slot = new Slot(item);
slots.Add(slot);
}
slot.IsInUse = false;
++freeSlotCount;
}
public int Count
{
get { return freeSlotCount; }
}
private void Advance()
{
position = (position + 1) % slots.Count;
}
class Slot
{
public Slot(T item)
{
this.Item = item;
}
public T Item { get; private set; }
public bool IsInUse { get; set; }
}
}
Saya bisa saja memilih sejumlah pendekatan yang berbeda, tetapi intinya adalah bahwa sumber daya harus diakses dalam urutan yang sama dengan yang dibuat, yang berarti bahwa kita harus mempertahankan referensi kepada mereka tetapi menandainya sebagai "sedang digunakan" (atau tidak ). Dalam skenario terburuk, hanya satu slot yang pernah tersedia, dan dibutuhkan pengulangan buffer penuh untuk setiap pengambilan. Ini buruk jika Anda memiliki ratusan sumber daya yang dikumpulkan dan memperoleh dan melepaskannya beberapa kali per detik; tidak benar-benar masalah untuk kumpulan 5-10 item, dan di kasus khas , di mana sumber daya digunakan dengan ringan, hanya perlu memajukan satu atau dua slot.
Ingat, kelas-kelas ini adalah kelas batin pribadi - itulah sebabnya mereka tidak perlu banyak memeriksa kesalahan, kumpulan itu sendiri membatasi akses ke mereka.
Lemparkan ke dalam enumerasi dan metode pabrik dan kita selesai dengan bagian ini:
// Outside the pool
public enum AccessMode { FIFO, LIFO, Circular };
private IItemStore itemStore;
// Inside the Pool
private IItemStore CreateItemStore(AccessMode mode, int capacity)
{
switch (mode)
{
case AccessMode.FIFO:
return new QueueStore(capacity);
case AccessMode.LIFO:
return new StackStore(capacity);
default:
Debug.Assert(mode == AccessMode.Circular,
"Invalid AccessMode in CreateItemStore");
return new CircularStore(capacity);
}
}
Masalah selanjutnya yang harus dipecahkan adalah strategi pemuatan. Saya telah mendefinisikan tiga jenis:
public enum LoadingMode { Eager, Lazy, LazyExpanding };
Dua yang pertama harus jelas; yang ketiga adalah semacam hibrida, itu memuat sumber daya tetapi tidak benar-benar mulai menggunakan kembali sumber daya apa pun sampai kolam penuh. Ini akan menjadi trade-off yang baik jika Anda ingin kumpulan menjadi penuh (yang sepertinya Anda lakukan) tetapi ingin menunda biaya untuk benar-benar membuatnya sampai akses pertama (yaitu untuk meningkatkan waktu startup).
Metode pemuatan benar-benar tidak terlalu rumit, karena sekarang kami memiliki abstraksi item-store:
private int size;
private int count;
private T AcquireEager()
{
lock (itemStore)
{
return itemStore.Fetch();
}
}
private T AcquireLazy()
{
lock (itemStore)
{
if (itemStore.Count > 0)
{
return itemStore.Fetch();
}
}
Interlocked.Increment(ref count);
return factory(this);
}
private T AcquireLazyExpanding()
{
bool shouldExpand = false;
if (count < size)
{
int newCount = Interlocked.Increment(ref count);
if (newCount <= size)
{
shouldExpand = true;
}
else
{
// Another thread took the last spot - use the store instead
Interlocked.Decrement(ref count);
}
}
if (shouldExpand)
{
return factory(this);
}
else
{
lock (itemStore)
{
return itemStore.Fetch();
}
}
}
private void PreloadItems()
{
for (int i = 0; i < size; i++)
{
T item = factory(this);
itemStore.Store(item);
}
count = size;
}
Bidang size
dan count
di atas merujuk pada ukuran maksimum kumpulan dan jumlah total sumber daya yang dimiliki oleh kumpulan ( masing-masing tidak harus tersedia ). AcquireEager
adalah yang paling sederhana, diasumsikan bahwa suatu barang sudah ada di toko - barang-barang ini akan dimuat pada saat konstruksi, yaitu diPreloadItems
metode yang ditunjukkan terakhir.
AcquireLazy
memeriksa untuk melihat apakah ada item gratis di kolam renang, dan jika tidak, itu membuat yang baru. AcquireLazyExpanding
akan membuat sumber daya baru selama pool belum mencapai ukuran targetnya. Saya sudah mencoba mengoptimalkan ini untuk meminimalkan penguncian, dan saya harap saya tidak membuat kesalahan (saya punya menguji ini di bawah kondisi multi-threaded, tapi jelas tidak lengkap).
Anda mungkin bertanya-tanya mengapa tidak ada metode ini yang repot-repot memeriksa untuk melihat apakah toko telah mencapai ukuran maksimum atau tidak. Saya akan membahasnya sebentar lagi.
Sekarang untuk kolam itu sendiri. Berikut ini adalah set lengkap data pribadi, beberapa di antaranya telah ditunjukkan:
private bool isDisposed;
private Func<Pool<T>, T> factory;
private LoadingMode loadingMode;
private IItemStore itemStore;
private int size;
private int count;
private Semaphore sync;
Menjawab pertanyaan yang saya sampaikan pada paragraf terakhir - bagaimana memastikan kita membatasi jumlah total sumber daya yang dibuat - ternyata .NET sudah memiliki alat yang sangat bagus untuk itu, itu disebut Semaphore dan dirancang khusus untuk memungkinkan perbaikan jumlah utas akses ke sumber daya (dalam hal ini "sumber daya" adalah toko item dalam). Karena kita tidak menerapkan antrian produsen / konsumen sepenuhnya, ini sangat memadai untuk kebutuhan kita.
Konstruktornya terlihat seperti ini:
public Pool(int size, Func<Pool<T>, T> factory,
LoadingMode loadingMode, AccessMode accessMode)
{
if (size <= 0)
throw new ArgumentOutOfRangeException("size", size,
"Argument 'size' must be greater than zero.");
if (factory == null)
throw new ArgumentNullException("factory");
this.size = size;
this.factory = factory;
sync = new Semaphore(size, size);
this.loadingMode = loadingMode;
this.itemStore = CreateItemStore(accessMode, size);
if (loadingMode == LoadingMode.Eager)
{
PreloadItems();
}
}
Seharusnya tidak ada kejutan di sini. Satu-satunya hal yang perlu diperhatikan adalah casing khusus untuk eager loading, menggunakanPreloadItems
metode yang sudah ditunjukkan sebelumnya.
Karena hampir semuanya telah diabstraksi secara bersih sekarang, metode Acquire
dan aktualnya Release
sangat mudah:
public T Acquire()
{
sync.WaitOne();
switch (loadingMode)
{
case LoadingMode.Eager:
return AcquireEager();
case LoadingMode.Lazy:
return AcquireLazy();
default:
Debug.Assert(loadingMode == LoadingMode.LazyExpanding,
"Unknown LoadingMode encountered in Acquire method.");
return AcquireLazyExpanding();
}
}
public void Release(T item)
{
lock (itemStore)
{
itemStore.Store(item);
}
sync.Release();
}
Seperti yang dijelaskan sebelumnya, kami menggunakan Semaphore
untuk mengontrol konkurensi alih-alih memeriksa status toko item secara agama. Selama item yang diperoleh dirilis dengan benar, tidak ada yang perlu dikhawatirkan.
Terakhir, ada pembersihan:
public void Dispose()
{
if (isDisposed)
{
return;
}
isDisposed = true;
if (typeof(IDisposable).IsAssignableFrom(typeof(T)))
{
lock (itemStore)
{
while (itemStore.Count > 0)
{
IDisposable disposable = (IDisposable)itemStore.Fetch();
disposable.Dispose();
}
}
}
sync.Close();
}
public bool IsDisposed
{
get { return isDisposed; }
}
Tujuan dari IsDisposed
properti itu akan menjadi jelas dalam sekejap. Semua Dispose
metode utama yang benar-benar dilakukan adalah membuang item yang dikumpulkan jika mereka menerapkan IDisposable
.
Sekarang Anda pada dasarnya dapat menggunakan ini apa adanya, dengan sebuah try-finally
blok, tapi saya tidak menyukai sintaks itu, karena jika Anda mulai membagikan sumber daya yang dikumpulkan antara kelas dan metode maka itu akan menjadi sangat membingungkan. Mungkin saja kelas utama yang menggunakan sumber daya tidak memilikinya referensi ke kumpulan. Ini benar-benar menjadi sangat berantakan, jadi pendekatan yang lebih baik adalah membuat objek yang dikumpulkan "pintar".
Katakanlah kita mulai dengan antarmuka / kelas sederhana berikut:
public interface IFoo : IDisposable
{
void Test();
}
public class Foo : IFoo
{
private static int count = 0;
private int num;
public Foo()
{
num = Interlocked.Increment(ref count);
}
public void Dispose()
{
Console.WriteLine("Goodbye from Foo #{0}", num);
}
public void Test()
{
Console.WriteLine("Hello from Foo #{0}", num);
}
}
Inilah Foo
sumber daya pura-pura sekali pakai kami yang mengimplementasikan IFoo
dan memiliki beberapa kode boilerplate untuk menghasilkan identitas unik. Apa yang kami lakukan adalah membuat objek khusus yang dikumpulkan:
public class PooledFoo : IFoo
{
private Foo internalFoo;
private Pool<IFoo> pool;
public PooledFoo(Pool<IFoo> pool)
{
if (pool == null)
throw new ArgumentNullException("pool");
this.pool = pool;
this.internalFoo = new Foo();
}
public void Dispose()
{
if (pool.IsDisposed)
{
internalFoo.Dispose();
}
else
{
pool.Release(this);
}
}
public void Test()
{
internalFoo.Test();
}
}
Ini hanya proksi semua metode "nyata" ke dalamnya IFoo
(kita bisa melakukan ini dengan perpustakaan Proxy Dinamis seperti Castle, tapi saya tidak akan membahasnya). Itu juga memelihara referensi ke Pool
yang membuatnya, sehingga ketika kita Dispose
objek ini, secara otomatis melepaskan dirinya kembali ke kolam. Kecuali ketika pool telah dibuang - ini berarti kita berada dalam mode "pembersihan" dan dalam hal ini sebenarnya membersihkan sumber daya internal sebagai gantinya.
Dengan menggunakan pendekatan di atas, kita dapat menulis kode seperti ini:
// Create the pool early
Pool<IFoo> pool = new Pool<IFoo>(PoolSize, p => new PooledFoo(p),
LoadingMode.Lazy, AccessMode.Circular);
// Sometime later on...
using (IFoo foo = pool.Acquire())
{
foo.Test();
}
Ini adalah hal yang sangat baik untuk dapat dilakukan. Ini berarti bahwa kode yang menggunakan satu IFoo
(yang bertentangan dengan kode yang menciptakan itu) tidak benar-benar perlu menyadari dari kolam renang. Anda bahkan dapat menyuntikkan IFoo
objek menggunakan perpustakaan DI favorit Anda dan Pool<T>
sebagai penyedia / pabrik.
Saya telah memasukkan kode lengkap pada PasteBin untuk kesenangan copy-and-paste Anda. Ada juga program pengujian singkat yang dapat Anda gunakan untuk bermain-main dengan berbagai mode pemuatan / akses dan kondisi multithreaded, untuk meyakinkan diri sendiri bahwa ini aman untuk benang dan tidak bermasalah.
Beri tahu saya jika Anda memiliki pertanyaan atau masalah tentang semua ini.