この質問は、いくつかの未知数のため、予想よりも少しトリッキーです:プールされるリソースの動作、オブジェクトの予想される/必要なライフタイム、プールが必要とされる実際の理由など。通常、プールは特別な目的-スレッドですプール、接続プールなど-リソースの動作を正確に理解している場合に最適化する方が簡単であり、さらに重要なことに、そのリソースの実装方法を制御できるためです。
それはそれほど単純ではないので、私が試みたのは、かなり柔軟なアプローチを提供して、実験して最も効果的なものを確認できるようにすることです。 長い投稿については事前にお詫びしますが、まともな汎用リソースプールの実装に関しては、対処すべき多くの根拠があります。ほんの表面を引っ掻いているだけです。
汎用プールには、次のようないくつかの主要な「設定」が必要です。
- リソース読み込み戦略-熱心または遅延。
- リソース読み込みメカニズム -実際にそれを構築する方法。
- アクセス戦略-「ラウンドロビン」と言っているが、これは思ったほど簡単ではない。この実装は、リソースが実際に再利用されるタイミングをプールが制御できないため、似ていますが完全ではない循環バッファーを使用できます。他のオプションはFIFOとLIFOです。FIFOにはランダムアクセスパターンが多く含まれますが、LIFOを使用すると、Least-Recently-Used解放戦略の実装が大幅に容易になります(これは範囲外であると言いましたが、言及する価値はあります)。
リソースのロードメカニズムについては、.NETは既に明確な抽象化を提供しています-デリゲート。
private Func<Pool<T>, T> factory;
これをプールのコンストラクターに渡すと、これでほぼ完了です。new()
制約付きのジェネリック型を使用することもできますが、これはより柔軟です。
他の2つのパラメーターのうち、アクセス戦略はより複雑な獣であるため、私のアプローチは継承(インターフェース)ベースのアプローチを使用することでした:
public class Pool<T> : IDisposable
{
// Other code - we'll come back to this
interface IItemStore
{
T Fetch();
void Store(T item);
int Count { get; }
}
}
ここでのコンセプトは単純Pool
です。スレッドの安全性などの一般的な問題はpublic クラスで処理しますが、アクセスパターンごとに異なる「アイテムストア」を使用します。LIFOはスタックで簡単に表現でき、FIFOはキューです。最適化されていませんが、おそらく適切ではない循環バッファー実装を使用しています。List<T>
です。ラウンドロビンアクセスパターンを概算するために、インデックスポインターを。
以下のクラスはすべての内部クラスです。Pool<T>
これはスタイルの選択でしたが、これらは実際にはの外部で使用するためのものではないためPool
、最も理にかなっています。
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);
}
}
これらは明白なものです-スタックとキュー。私は彼らが本当に多くの説明を正当化するとは思わない。循環バッファはもう少し複雑です:
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; }
}
}
いくつかの異なるアプローチを選択できたかもしれませんが、肝心なことは、リソースは作成されたのと同じ順序でアクセスする必要があるということです。 )。最悪のシナリオでは、利用可能なスロットは1つだけであり、フェッチごとにバッファーの完全な反復が必要です。何百ものリソースがプールされていて、それらを1秒あたり数回取得および解放している場合、これは悪いことです。5〜10アイテムのプールでは実際には問題ではなく、リソースが軽く使用される一般的なケースでは、1つまたは2つのスロットを進めるだけで済みます。
これらのクラスはプライベートな内部クラスであることを忘れないでください。そのため、多くのエラーチェックを必要とせず、プール自体がそれらへのアクセスを制限しています。
列挙とファクトリーメソッドをスローすると、この部分は完了です。
// 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);
}
}
解決する次の問題は、ローディング戦略です。私は3つのタイプを定義しました:
public enum LoadingMode { Eager, Lazy, LazyExpanding };
最初の2つは自明です。3番目は一種のハイブリッドです。リソースを遅延ロードしますが、プールがいっぱいになるまで実際にはリソースの再利用を開始しません。これは、プールを一杯にしたい(そうするように聞こえる)が、最初にアクセスするまで(つまり、起動時間を改善するために)実際にプールを作成する費用を延期したい場合に、適切なトレードオフになります。
ローディングメソッドはそれほど複雑ではありませんが、アイテムストアの抽象化があります:
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;
}
上記のsize
およびcount
フィールドは、それぞれ、プールの最大サイズと、プールが所有する(ただし、必ずしも利用可能ではない)リソースの総数を示します。 AcquireEager
最も単純です。アイテムがすでにストアにあると想定しています-これらのアイテムは構築時に、つまりPreloadItems
最後に示したメソッドでプリロードされます。
AcquireLazy
プールに空きアイテムがあるかどうかを確認し、ない場合は新しいアイテムを作成します。 AcquireLazyExpanding
プールがまだターゲットサイズに達していない限り、新しいリソースが作成されます。私はこれを最適化してロックを最小限に抑えようとしましたが、私がミスを犯していないことを願っています(私はマルチスレッドの条件でこれテストしましたが、完全ではありません)。
これらのメソッドのいずれも、ストアが最大サイズに達したかどうかを確認する必要がないのではないかと不思議に思うかもしれません。それについてはすぐに説明します。
次に、プール自体についてです。以下に、プライベートデータの完全なセットを示します。一部はすでに表示されています。
private bool isDisposed;
private Func<Pool<T>, T> factory;
private LoadingMode loadingMode;
private IItemStore itemStore;
private int size;
private int count;
private Semaphore sync;
前の段落で説明した質問に答えます-作成されるリソースの総数を制限する方法-.NETにはすでにそのための完全に優れたツールがあり、Semaphoreと呼ばれ、修正を許可するように特別に設計されていますリソースにアクセスするスレッドの数(この場合、「リソース」は内部のアイテムストアです)。フルオンのプロデューサー/コンシューマーキューを実装していないため、これは私たちのニーズに完全に適しています。
コンストラクターは次のようになります。
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();
}
}
ここに驚きはないはずです。注意すべき唯一のことは、熱心なローディングのための特別なケーシングであり、PreloadItems
すでに前に示し方法。
ほとんどすべてが今ではきれいに抽象化されているので、実際のAcquire
およびRelease
メソッドは本当に非常に簡単です:
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();
}
前に説明したようSemaphore
に、アイテムストアのステータスを厳密にチェックする代わりに、並行性の制御にを使用しています。入手したアイテムが正しくリリースされる限り、心配することは何もありません。
最後になりましたが、クリーンアップがあります:
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; }
}
そのIsDisposed
プロパティの目的は、すぐに明らかになります。メインのDispose
メソッドが実際に行うことは、実際にプールされたアイテムを実装する場合、それらを破棄することだけIDisposable
です。
これで、try-finally
ブロックを使用して基本的にこれをそのまま使用できますが、クラスとメソッドの間でプールされたリソースを渡し始めると非常に混乱するため、その構文は好きではありません。リソースを使用するメインクラスが持っていない可能性もありますプールへの参照あります。これは実際にはかなり厄介なものになるため、「スマートな」プールされたオブジェクトを作成することをお勧めします。
次の簡単なインターフェース/クラスから始めるとしましょう:
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);
}
}
Foo
これはIFoo
、一意のIDを生成するための定型コードを実装して持つ、使い捨ての偽装リソースです。私たちがすることは、別の特別なプールされたオブジェクトを作成することです:
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();
}
}
これは、すべての「実際の」メソッドをその内部にIFoo
プロキシするだけです(Castleのようなダイナミックプロキシライブラリを使用してこれを行うことができますが、これには入りません)。また、Pool
それを作成するへの参照を維持するため、Dispose
このオブジェクトの場合、自動的に解放されてプールに戻されます。 プールがすでに破棄されている場合を除いて -これは、「クリーンアップ」モードであることを意味し、この場合、実際には内部リソースをクリーンアップします。
上記のアプローチを使用して、次のようなコードを記述します。
// 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();
}
これは非常に良いことです。これは、IFoo
(それを作成するコードとは対照的に)を使用するコードが実際にプールを認識する必要がないことを意味します。お気に入りのDIライブラリを使用して、プロバイダー/ファクトリーとしてオブジェクトを注入 することもできIFoo
ますPool<T>
。
私が置かれているペーストビン上の完全なコードをコピー&ペーストの楽しみのために。また、さまざまなロード/アクセスモードやマルチスレッド条件をいじって、スレッドセーフでバグがないことを確認するために使用できる短いテストプログラムもあります。
これについて質問や懸念がある場合はお知らせください。