誰かがSQL接続プーリングの制限されたリソースの共有オブジェクトプール戦略を実装する上で良いリソースを持っていますか? (つまり、スレッドセーフであることが完全に実装されます)。
明確化のための@Aaronaught要求に関してフォローアップするには、外部サービスへの要求を負荷分散するためにプールを使用します。私の直接の状況とは対照的に、おそらくすぐに理解しやすいシナリオにそれを置くこと。 NHibernateのISession
オブジェクトと同様に機能するセッションオブジェクトがあります。一意の各セッションは、データベースへの接続を管理します。現在、1つの長時間実行されるセッションオブジェクトがあり、サービスプロバイダーがこの個々のセッションの使用を制限しているという問題が発生しています。
単一のセッションが長期のサービスアカウントとして扱われるという期待がないため、彼らは明らかに、それをサービスをハンマーしているクライアントとして扱います。ここで私の質問になりますが、1つの個別のセッションを持つ代わりに、異なるセッションのプールを作成し、以前のように単一のフォーカルポイントを作成するのではなく、それらの複数のセッションにサービスにリクエストを分割します。
背景が何らかの価値を提供することを願っていますが、あなたの質問のいくつかに直接答えてください:
Q:オブジェクトの作成は高価ですか?
A:オブジェクトは限られたリソースのプールではありません
Q:頻繁に取得/リリースされますか?
A:はい、もう一度、通常はすべての単一ページ要求の期間中に1が取得および解放されるNHibernate ISessionsについて考えることができます。
Q:単純な先着順で十分ですか、それとももっとインテリジェントなものが必要ですか、それは飢starを防ぐでしょうか?
A:飢ationにより、単純なラウンドロビン型の配布で十分です。発信者がリリースを待ってブロックされる利用可能なセッションがない場合を想定しています。セッションは異なる呼び出し元で共有できるため、これは実際には適用できません。私の目標は、1つの単一セッションではなく、複数のセッションに使用を分散することです。
これはおそらくオブジェクトプールの通常の使用とは異なるため、元々この部分を省き、飢the状態が発生するのを許すのではなく、オブジェクトを共有できるようにパターンを調整するだけでした。
Q:優先順位、遅延読み込みと熱心な読み込みなどのことはどうですか?
A:簡単にするために、プール自体の作成時に使用可能なオブジェクトのプールを作成すると仮定してください。
dotnet core には、ベースクラスライブラリ(BCL)に追加されたオブジェクトプーリングの実装があります。元のGitHubの問題 here を読んで、 System.Buffers のコードを表示できます。現在、ArrayPool
は使用可能な唯一のタイプであり、配列のプールに使用されます。ニースのブログ投稿があります こちら 。
namespace System.Buffers
{
public abstract class ArrayPool<T>
{
public static ArrayPool<T> Shared { get; internal set; }
public static ArrayPool<T> Create(int maxBufferSize = <number>, int numberOfBuffers = <number>);
public T[] Rent(int size);
public T[] Enlarge(T[] buffer, int newSize, bool clearBuffer = false);
public void Return(T[] buffer, bool clearBuffer = false);
}
}
その使用例は、ASP.NET Coreで見ることができます。 ASP.NET Coreは、ドットネットコアBCL内にあるため、オブジェクトプールをNewtonsoft.JsonのJSONシリアライザーなどの他のオブジェクトと共有できます。 Newtonsoft.Jsonがこれをどのように行っているかの詳細については、 this ブログ投稿をご覧ください。
新しいMicrosoft Roslyn C#コンパイラには、 ObjectPool タイプが含まれています。これは、頻繁に使用されるオブジェクトをプールするために使用されます。これにより、発生する必要があるガベージコレクション操作の量とサイズが削減されます。 ObjectPoolを使用するいくつかの異なるサブ実装があります(参照: Roslynでオブジェクトプーリングの実装が非常に多いのはなぜですか? )。
1- SharedPools -20個のオブジェクトのプール、またはBigDefaultが使用されている場合は100個のプールを格納します。
// Example 1 - In a using statement, so the object gets freed at the end.
using (PooledObject<Foo> pooledObject = SharedPools.Default<List<Foo>>().GetPooledObject())
{
// Do something with pooledObject.Object
}
// Example 2 - No using statement so you need to be sure no exceptions are not thrown.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
// Do something with list
SharedPools.Default<List<Foo>>().Free(list);
// Example 3 - I have also seen this variation of the above pattern, which ends up the same as Example 1, except Example 1 seems to create a new instance of the IDisposable [PooledObject<T>][4] object. This is probably the preferred option if you want fewer GC's.
List<Foo> list = SharedPools.Default<List<Foo>>().AllocateAndClear();
try
{
// Do something with list
}
finally
{
SharedPools.Default<List<Foo>>().Free(list);
}
2- ListPool および StringBuilderPool -厳密に分離された実装ではなく、ListおよびStringBuilder専用の上記のSharedPools実装のラッパー。したがって、これはSharedPoolsに保存されているオブジェクトのプールを再利用します。
// Example 1 - No using statement so you need to be sure no exceptions are thrown.
StringBuilder stringBuilder= StringBuilderPool.Allocate();
// Do something with stringBuilder
StringBuilderPool.Free(stringBuilder);
// Example 2 - Safer version of Example 1.
StringBuilder stringBuilder= StringBuilderPool.Allocate();
try
{
// Do something with stringBuilder
}
finally
{
StringBuilderPool.Free(stringBuilder);
}
3- PooledDictionary および PooledHashSet -これらはObjectPoolを直接使用し、オブジェクトの完全に独立したプールを持ちます。 128個のオブジェクトのプールを格納します。
// Example 1
PooledHashSet<Foo> hashSet = PooledHashSet<Foo>.GetInstance()
// Do something with hashSet.
hashSet.Free();
// Example 2 - Safer version of Example 1.
PooledHashSet<Foo> hashSet = PooledHashSet<Foo>.GetInstance()
try
{
// Do something with hashSet.
}
finally
{
hashSet.Free();
}
このライブラリは、MemoryStream
オブジェクトのプーリングを提供します。 System.IO.MemoryStream
のドロップイン代替品です。セマンティクスはまったく同じです。 Bingのエンジニアによって設計されました。ブログの投稿を読む here または GitHub のコードを参照してください。
var sourceBuffer = new byte[]{0,1,2,3,4,5,6,7};
var manager = new RecyclableMemoryStreamManager();
using (var stream = manager.GetStream())
{
stream.Write(sourceBuffer, 0, sourceBuffer.Length);
}
RecyclableMemoryStreamManager
は一度宣言する必要があり、プロセス全体に渡って存続することに注意してください。これがプールです。必要に応じて、複数のプールを使用しても問題ありません。
この質問は、プールされているリソースの振る舞い、オブジェクトの予想/必要なライフタイム、プールが必要とされる本当の理由など、いくつかの未知の理由で予想されるよりも少し複雑です。通常、プールは特別な目的-スレッドプール、接続プールなど-リソースが何を行うかを正確に知っていて、さらに重要なことに、そのリソースの実装方法に関してcontrolを持っていると、最適化が容易になります。
それはそれほど単純ではないので、私がやろうとしたことは、かなり柔軟なアプローチを提供することです。 長い投稿には前もって謝罪しますが、まともな汎用リソースプールの実装に関しては、カバーすべき多くの根拠があります。そして、私は本当に表面をひっかくだけです。
汎用プールには、次のようないくつかの主要な「設定」が必要です。
リソースのロードメカニズムについては、.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
クラスにスレッドセーフなどの一般的な問題を処理させますが、アクセスパターンごとに異なる「アイテムストア」を使用します。 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個のアイテムのプールでは実際には問題ではなく、リソースが軽く使用されるtypicalの場合、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
フィールドは、それぞれプールの最大サイズと、プールが所有するリソースの総数を示します(ただし、必ずしもavailableとは限りません)。 AcquireEager
は最も単純で、アイテムが既にストアにあると仮定します。これらのアイテムは、構築時に、つまり最後に示されたPreloadItems
メソッドにプリロードされます。
AcquireLazy
は、プールに空きアイテムがあるかどうかを確認し、ない場合は新しいアイテムを作成します。 AcquireLazyExpanding
は、プールがまだ目標サイズに達していない限り、新しいリソースを作成します。ロックを最小限に抑えるためにこれを最適化しようとしましたが、間違いを犯していないことを願っています(私はhaveこれをマルチスレッド条件でテストしましたが、明らかに網羅的ではありません)。
これらのメソッドのいずれも、ストアが最大サイズに達したかどうかを確認する手間をかけないのはなぜでしょうか。これについてはすぐに説明します。
次に、プール自体について説明します。プライベートデータの完全なセットを次に示します。その一部はすでに表示されています。
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
ブロックでこのまま使用できますが、その構文は好きではありません。プールされたリソースをクラスとメソッドの間でやり取りし始めると、非常に混乱するからです。リソースを使用するメインクラスがhaveプールへの参照でさえない可能性があります。本当に面倒になりますので、「スマートな」プールされたオブジェクトを作成することをお勧めします。
次のシンプルなインターフェイス/クラスから始めましょう。
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
を実装し、一意のIDを生成するための定型コードを持っているふりをする使い捨てIFoo
リソースを次に示します。別の特別なプールされたオブジェクトを作成します:
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
すると、自動的に解放されてプールに戻ります。 Exceptプールがすでに破棄されている場合-これは、「クリーンアップ」モードになっていることを意味し、この場合は実際に内部をクリーンアップします代わりにリソース。
上記のアプローチを使用して、次のようなコードを作成します。
// 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();
}
これはveryできることです。つまり、ses the IFoo
(コードを作成するコードとは対照的に)が実際にプールを意識する必要はありません。お気に入りのDIライブラリとプロバイダ/工場としてPool<T>
を使用して、オブジェクトをinjectIFoo
オブジェクトにすることもできます。
コピーアンドペーストを楽しむために Pastebinの完全なコード を付けました。また、短い テストプログラム を使用して、さまざまな読み込み/アクセスモードとマルチスレッド条件で遊んで、スレッドセーフでバグがないことを確認できます。
これについて質問や懸念がある場合はお知らせください。
このような何かがあなたのニーズに合うかもしれません。
/// <summary>
/// Represents a pool of objects with a size limit.
/// </summary>
/// <typeparam name="T">The type of object in the pool.</typeparam>
public sealed class ObjectPool<T> : IDisposable
where T : new()
{
private readonly int size;
private readonly object locker;
private readonly Queue<T> queue;
private int count;
/// <summary>
/// Initializes a new instance of the ObjectPool class.
/// </summary>
/// <param name="size">The size of the object pool.</param>
public ObjectPool(int size)
{
if (size <= 0)
{
const string message = "The size of the pool must be greater than zero.";
throw new ArgumentOutOfRangeException("size", size, message);
}
this.size = size;
locker = new object();
queue = new Queue<T>();
}
/// <summary>
/// Retrieves an item from the pool.
/// </summary>
/// <returns>The item retrieved from the pool.</returns>
public T Get()
{
lock (locker)
{
if (queue.Count > 0)
{
return queue.Dequeue();
}
count++;
return new T();
}
}
/// <summary>
/// Places an item in the pool.
/// </summary>
/// <param name="item">The item to place to the pool.</param>
public void Put(T item)
{
lock (locker)
{
if (count < size)
{
queue.Enqueue(item);
}
else
{
using (item as IDisposable)
{
count--;
}
}
}
}
/// <summary>
/// Disposes of items in the pool that implement IDisposable.
/// </summary>
public void Dispose()
{
lock (locker)
{
count = 0;
while (queue.Count > 0)
{
using (queue.Dequeue() as IDisposable)
{
}
}
}
}
}
使用例
public class ThisObject
{
private readonly ObjectPool<That> pool = new ObjectPool<That>(100);
public void ThisMethod()
{
var that = pool.Get();
try
{
// Use that ....
}
finally
{
pool.Put(that);
}
}
}
MSDNのサンプル: 方法:ConcurrentBagを使用してオブジェクトプールを作成する
かつてマイクロソフトは、Microsoft Transaction Server(MTS)およびそれ以降のCOM +を通じて、COMオブジェクトのオブジェクトプーリングを行うフレームワークを提供していました。この機能は、.NET FrameworkのSystem.EnterpriseServicesに引き継がれ、現在はWindows Communication Foundationでも使用されています。
この記事は.NET 1.1からのものですが、フレームワークの現在のバージョンにも適用されるはずです(WCFが推奨される方法ですが)。
私はAronaughtの実装が本当に好きです。特に、セマフォを使用することでリソースが利用可能になるのを待っているためです。いくつかの追加があります。
sync.WaitOne()
をsync.WaitOne(timeout)
に変更し、Acquire(int timeout)
メソッドのパラメーターとしてタイムアウトを公開します。これには、スレッドがタイムアウトしてオブジェクトが利用可能になるのを待つときに条件を処理する必要もあります。Recycle(T item)
メソッドを追加します。これは、プール内のオブジェクトの数が限られている別の実装です。
public class ObjectPool<T>
where T : class
{
private readonly int maxSize;
private Func<T> constructor;
private int currentSize;
private Queue<T> pool;
private AutoResetEvent poolReleasedEvent;
public ObjectPool(int maxSize, Func<T> constructor)
{
this.maxSize = maxSize;
this.constructor = constructor;
this.currentSize = 0;
this.pool = new Queue<T>();
this.poolReleasedEvent = new AutoResetEvent(false);
}
public T GetFromPool()
{
T item = null;
do
{
lock (this)
{
if (this.pool.Count == 0)
{
if (this.currentSize < this.maxSize)
{
item = this.constructor();
this.currentSize++;
}
}
else
{
item = this.pool.Dequeue();
}
}
if (null == item)
{
this.poolReleasedEvent.WaitOne();
}
}
while (null == item);
return item;
}
public void ReturnToPool(T item)
{
lock (this)
{
this.pool.Enqueue(item);
this.poolReleasedEvent.Set();
}
}
}
Java指向のこの記事では、connectionImplプールパターンと抽象化されたオブジェクトプールパターンを公開しており、最初のアプローチとして適しています。 http://www.developer.com/design/article.php/626171/Pattern-Summaries- Object-Pool.htm
オブジェクトプールパターン:
ConcurrentBagを使用してオブジェクトプールを作成するmsdnの拡張方法。