Bu soru, birkaç bilinmeyen nedeniyle beklenenden biraz daha zor: Toplanan kaynağın davranışı, nesnelerin beklenen / gerekli ömrü, havuzun gerekli olmasının gerçek nedeni, vb. Tipik olarak havuzlar özel amaçlıdır vb havuzları, bağlantı havuzları, - Eğer kaynak yapar tam olarak ne olduğunu ve daha da önemlisi var ne zaman birini optimize etmek kolaydır, çünkü kontrolünü bu kaynağın nasıl uygulandığını üzerinde.
Bu kadar basit olmadığından, yapmaya çalıştığım, deneyebileceğiniz ve neyin en iyi çalıştığını görebileceğiniz oldukça esnek bir yaklaşım sunmaktır. Uzun yazı için şimdiden özür dileriz, ancak genel amaçlı iyi bir kaynak havuzunun uygulanması söz konusu olduğunda kapsayacak çok şey vardır. ve gerçekten sadece yüzeyi çiziyorum.
Genel amaçlı bir havuzun birkaç ana "ayarına" sahip olması gerekir:
- Kaynak yükleme stratejisi - istekli veya tembel;
- Kaynak yükleme mekanizması - gerçekte nasıl inşa edileceği;
- Erişim stratejisi - göründüğü kadar basit olmayan "yuvarlak robin" den bahsediyorsunuz; bu uygulama benzer , ancak mükemmel olmayan dairesel bir arabellek kullanabilir , çünkü havuzun kaynakların ne zaman geri kazanıldığı konusunda kontrolü yoktur. Diğer seçenekler FIFO ve LIFO; FIFO'nun rasgele erişimli bir modeli daha olacak, ancak LIFO, En Son Kullanılanlar için kullanılan bir serbest bırakma stratejisinin uygulanmasını önemli ölçüde kolaylaştırıyor (ki bunun kapsam dışında olduğunu söylediniz, ancak yine de bahsetmeye değer).
Kaynak yükleme mekanizması için .NET zaten temiz bir soyutlama sağlar - delegeler.
private Func<Pool<T>, T> factory;
Bunu havuzun yapıcısından geçirin ve işimiz bitti. new()
Kısıtlamalı genel bir tür kullanmak da işe yarar, ancak bu daha esnektir.
Diğer iki parametreden, erişim stratejisi daha karmaşık bir canavardır, bu yüzden yaklaşımım kalıtım (arayüz) tabanlı bir yaklaşım kullanmaktı:
public class Pool<T> : IDisposable
{
// Other code - we'll come back to this
interface IItemStore
{
T Fetch();
void Store(T item);
int Count { get; }
}
}
Buradaki kavram basittir - ortak Pool
sınıfın iş parçacığı güvenliği gibi ortak sorunları ele almasına izin vereceğiz , ancak her erişim modeli için farklı bir "eşya deposu" kullanacağız. LIFO kolayca bir yığınla temsil edilir, FIFO bir kuyruktur ve List<T>
bir yuvarlak erişim erişim modeline yaklaşmak için bir ve dizin işaretçisi kullanarak çok optimize edilmemiş ancak muhtemelen yeterli bir dairesel tampon uygulaması kullandım .
Aşağıdaki tüm sınıflar iç sınıflardır Pool<T>
- bu bir stil seçimiydi, ancak bunlar gerçekten dışarıda kullanılmadığı Pool
için en mantıklı.
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);
}
}
Bunlar bariz olanlardır - yığın ve kuyruk. Gerçekten çok fazla açıklama gerektirdiğini sanmıyorum. Dairesel tampon biraz daha karmaşıktır:
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; }
}
}
Bir dizi farklı yaklaşım seçebilirdim, ancak sonuçta kaynaklara yaratıldıkları sırayla erişilmesi gerekiyor, bu da onlara referansları sürdürmemiz, ancak bunları "kullanımda" olarak işaretlememiz gerektiği (veya ). En kötü senaryoda, yalnızca bir yuva kullanılabilir ve her getirme için arabelleğin tam bir yinelemesini alır. Yüzlerce kaynağınız toplanmışsa ve saniyede birkaç kez edinip serbest bırakırsanız bu kötüdür; 5-10 maddelik bir havuz için bir sorun değil ve kaynakların hafifçe kullanıldığı tipik durumda, sadece bir veya iki yuva ilerletmek zorunda.
Unutmayın, bu sınıflar özel iç sınıflardır - bu yüzden çok fazla hata kontrolüne ihtiyaç duymazlar, havuzun kendisi bunlara erişimi kısıtlar.
Bir numaralandırma ve bir fabrika yöntemiyle atın ve bu kısmı bitirdik:
// 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);
}
}
Çözülmesi gereken bir sonraki sorun yükleme stratejisidir. Üç tür tanımladım:
public enum LoadingMode { Eager, Lazy, LazyExpanding };
İlk ikisi kendi kendini açıklayıcı olmalıdır; üçüncüsü bir çeşit melezdir, kaynakları tembel yükler, ancak havuz dolana kadar herhangi bir kaynağı yeniden kullanmaya başlamaz. Havuzun tam olmasını istiyorsanız (ki bu sizin yaptığınız gibi geliyor) ama ilk erişime kadar onları oluşturma maliyetini ertelemek istiyorsanız (yani başlangıç zamanlarını iyileştirmek) bu iyi bir değiş tokuş olacaktır.
Yükleme yöntemleri gerçekten çok karmaşık değil, şimdi eşya deposu soyutlamasına sahibiz:
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;
}
Yukarıdaki size
ve count
alanları, havuzun maksimum boyutunu ve havuzun sahip olduğu toplam kaynak sayısını (ancak zorunlu olarak mevcut değildir ) ifade eder. AcquireEager
en basit olanı, bir öğenin zaten mağazada olduğunu varsayar - bu öğeler inşaatta, yani en PreloadItems
son gösterilen yöntemde önceden yüklenir .
AcquireLazy
havuzda ücretsiz öğeler olup olmadığını kontrol eder ve yoksa yeni bir öğe oluşturur. AcquireLazyExpanding
havuz henüz hedef boyutuna ulaşmadığı sürece yeni bir kaynak oluşturur. Ben kilitleme aza indirmek için bu optimize etmek denedim ve ben hiç hata (ı yapmadınız umut var çok kanallı şartlarda bu test, ama besbelli değil etraflıca).
Bu yöntemlerin hiçbirinin mağazanın maksimum boyuta ulaşıp ulaşmadığını görmek için neden uğraşmadığını merak ediyor olabilirsiniz. Birazdan buna gireceğim.
Şimdi havuzun kendisi için. İşte bazıları zaten gösterilen özel verilerin tamamı:
private bool isDisposed;
private Func<Pool<T>, T> factory;
private LoadingMode loadingMode;
private IItemStore itemStore;
private int size;
private int count;
private Semaphore sync;
Son paragrafta ele aldığım soruyu cevaplayarak - yaratılan toplam kaynak sayısını sınırlandırmayı nasıl garanti ederiz - .NET'in zaten bunun için mükemmel bir araca sahip olduğu ortaya çıkıyor, buna Semaphore deniyor ve özellikle bir sabitlemeye izin vermek için tasarlandı bir kaynağa erişim iş parçacığı sayısı (bu durumda "kaynak" iç öğe deposudur). Tam üretici / tüketici kuyruğu uygulamadığımızdan, bu ihtiyaçlarımız için mükemmel bir şekilde yeterlidir.
Yapıcı şöyle görünür:
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();
}
}
Burada sürpriz olmamalı. Dikkat edilmesi gereken tek şey, PreloadItems
daha önce gösterilen yöntemi kullanarak istekli yükleme için özel kasa .
Neredeyse her şey şimdiye kadar temiz bir şekilde soyutlandığından, gerçek Acquire
ve Release
yöntemler gerçekten çok basittir:
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();
}
Daha önce açıklandığı gibi, Semaphore
eşya mağazasının durumunu dini olarak kontrol etmek yerine eşzamanlılığı kontrol etmek için kullanıyoruz. Edinilen öğeler doğru bir şekilde serbest bırakıldığı sürece endişelenecek bir şey yok.
Son fakat en az değil, temizleme var:
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; }
}
Bu IsDisposed
mülkün amacı bir anda netleşecek. Dispose
Gerçekten yaptığı tüm ana yöntem, eğer gerçek havuzlanmış öğeleri uygulamaktır IDisposable
.
Şimdi temelde bunu bir try-finally
blokla olduğu gibi kullanabilirsiniz , ancak bu sözdiziminden hoşlanmıyorum, çünkü sınıflar ve yöntemler arasında havuzlanmış kaynaklardan geçmeye başlarsanız çok kafa karıştırıcı olacaktır. Bu bir kaynak kullandığı ana sınıfı bile etmediğini mümkündür sahip havuza bir başvuru. Gerçekten dağınık hale gelir, bu yüzden daha iyi bir yaklaşım "akıllı" bir havuzlu nesne yaratmaktır.
Diyelim ki şu basit arayüz / sınıfla başlıyoruz:
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);
}
}
İşte benzersiz kimlikler üretmek için bazı Foo
kaynak IFoo
kodu uygulayan ve sahip olan taklit tek kullanımlık kaynağımız . Yaptığımız başka bir özel, havuzlanmış nesne oluşturmak:
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();
}
}
Bu sadece tüm "gerçek" yöntemleri kendi içine yakınlaştırır IFoo
(bunu Castle gibi bir Dinamik Proxy kütüphanesi ile yapabiliriz, ama buna girmeyeceğim). Ayrıca Pool
, onu oluşturana bir referans tutar , böylece Dispose
bu nesneyi otomatik olarak havuza geri bırakır. Hariç havuz zaten elden çıkarılan kaldığında - biz "temizleme" modunda ve aslında bu durumda olan bu araçlar , iç kaynak temizler yerine.
Yukarıdaki yaklaşımı kullanarak, şöyle bir kod yazacağız:
// 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();
}
Bunu yapabilmek çok iyi bir şey. Bu kod anlamına gelir kullanırIFoo
(bunu yaratan kod aksine) aslında havuz farkında olması gerekmez. En sevdiğiniz DI kitaplığını ve sağlayıcı / fabrika olarak kullanarak nesne enjekte edebilirsiniz .IFoo
Pool<T>
Kopyalama ve yapıştırma keyfi için tam kodu PasteBin'e koydum . Ayrıca , farklı yükleme / erişim modları ve çok iş parçacıklı koşullarla oynamak, iş parçacığı açısından güvenli ve buggy olmadığından emin olmak için kullanabileceğiniz kısa bir test programı da vardır .
Bunlardan herhangi biriyle ilgili herhangi bir sorunuz veya endişeniz varsa bize bildirin.