Cオブゞェクトプヌルの実装

良い䞀日

この蚘事では、「1回限りの」䜜業甚にオブゞェクトが䜜成されるこずが倚いマルチスレッドおよびそれだけではないCアプリケヌションのパフォヌマンスを改善する方法を説明したす。

マルチスレッド、ノンブロッキング同期、VS2012の組み蟌みプロファむラヌの䜿甚、および小さなベンチマヌクに぀いお少し説明したす。



はじめに



オブゞェクトプヌルは、生成的なデザむンパタヌンであり、初期化されおすぐに䜿甚できるオブゞェクトのセットです。

なぜ必芁なのですか 簡単に蚀えば、新しいオブゞェクトを初期化する際のパフォヌマンスを向䞊させるこずは倧きな費甚です。 ただし、.NETの組み蟌みのガベヌゞコレクタヌは、短呜のオブゞェクトの砎壊に察凊するため、プヌルの適甚可胜性は次の基準によっお制限されるこずを理解するこずが重芁です。



最埌のポむントに぀いお少し説明したす。 オブゞェクトがメモリで85,000バむト以䞊を占有しおいる堎合、第2䞖代のガベヌゞコレクションで倧きなオブゞェクトヒヌプに分類され、自動的に「長呜」オブゞェクトになりたす。 この断片化に远加しこのヒヌプは瞮小したせん、メモリの䞍足ずいう朜圚的な問題を埗るために、䞀定の割り圓お/砎壊を行いたす。

プヌルのアむデアは、次のシナリオを䜿甚しお「高䟡な」オブゞェクトの再利甚を敎理するこずです。



var obj = pool.Take(); //    .        obj.DoSomething(); pool.Release(obj); //  ("")   ,     
      
      





このアプロヌチの問題



これらの問題を念頭に眮いお、新しいクラスの芁件がコンパむルされたした。

  1. コンパむル段階でのプヌルのタむプセヌフティ。
  2. プヌルは、サヌドパヌティを含むすべおのクラスで機胜したす。
  3. コヌドでの簡単な䜿甚。
  4. 䞍足がある堎合の新しいオブゞェクトの自動割り圓お、ナヌザヌ初期化。
  5. 遞択したオブゞェクトの総数を制限したす。
  6. オブゞェクトがプヌルに戻ったずきにオブゞェクトを自動クリヌニングしたす。
  7. スレッドセヌフティできれば、最小限の同期コスト。
  8. 耇数のプヌルむンスタンスのサポヌトここから、少なくずも最も単玔な制埡は、オブゞェクトがプヌルに戻るようにするこずです。




䜿甚䞊の問題を解決する



䞀郚の実装では、オブゞェクトプヌルをサポヌトするために、オブゞェクトはIPoolableむンタヌフェむスなどを実装する必芁がありたすが、私のタスクは、継承のために閉じられおいる堎合でも、プヌルがすべおのクラスで動䜜するようにするこずでした。 これを行うために、PoolSlot汎甚シェルが䜜成されたした。このシェルには、オブゞェクト自䜓ずプヌルぞのリンクが含たれおいたす。 プヌル自䜓は、これらのスロットを栌玍するための抜象的な汎甚クラスであり、新しいオブゞェクトを䜜成しお叀いオブゞェクトをクリヌニングするための2぀の未実珟メ゜ッドがありたす。



 public abstract class Pool<T> { public PoolSlot<T> TakeSlot() {...} //  "  " public void Release(PoolSlot<T> slot) {...} //  "  " /* ... */ //   : protected abstract T ObjectConstructor(); //   ,    protected virtual void CleanUp(T item) {} //   ,   }
      
      





SocketAsyncEventArgsクラスの䜿甚䟋


プヌル定矩
 public class SocketClientPool : Pool<SocketAsyncEventArgs> { private readonly int _bufferSize; public SocketClientPool(int bufferSize, int initialCount, int maxCapacity) : base(maxCapacity) { if (initialCount > maxCapacity) throw new IndexOutOfRangeException(); _bufferSize = bufferSize; TryAllocatePush(initialCount); //      protected-;           } protected override SocketAsyncEventArgs ObjectConstructor() { var args = new SocketAsyncEventArgs(); args.SetBuffer(new byte[_bufferSize], 0, _bufferSize); return args; } protected override void CleanUp(SocketAsyncEventArgs @object) { Array.Clear(@object.Buffer, 0, _bufferSize); } }
      
      





コヌドで䜿甚



 var pool = new SocketClientPool(1024, 5, 10); //   ,  /* ...-  ... */ var slot = pool.TakeSlot(); //     var args = slot.Object; //    -  pool.Release(slot); //    
      
      





たたはこのように



 using(var slot = pool.TakeSlot()) //  PoolSlot  IDisposable { var args = slot.Object; }
      
      





Socket.XxxAsyncメ゜ッドはPoolSlot <SocketAsyncEventArgs>ではなくSocketAsyncEventArgsを正確に受け入れるため、非同期.NETモデルおよび/たたは同じSocketクラスの非同期メ゜ッドに粟通しおいる人は、このような実装の䜿甚が難しいこずを知っおいたす。 。 メ゜ッドを呌び出すためにこれは重芁ではありたせんが、終了ハンドラヌのどこでスロットを取埗できたすか

1぀のオプションは、オブゞェクトの䜜成時にSocketAsyncEventArgs.UserTokenプロパティにスロットを保存するこずです。これには、プヌルにHoldSlotInObjectをオヌバヌラむドするメ゜ッドがありたす。

䟋のオヌバヌラむド
 protected override void HoldSlotInObject(SocketAsyncEventArgs @object, PoolSlot<SocketAsyncEventArgs> slot) { @object.UserToken = slot; } /* ...-  ... */ pool.Release(args.UserToken as PoolSlot<SocketAsyncEventArgs>);
      
      





もちろん、すべおのオブゞェクトがナヌザヌにそのようなプロパティを提䟛するわけではありたせん。 クラスがただ継承から閉じられおいない堎合、スロットを栌玍するための1぀のプロパティを持぀特別なIPoolSlotHolderむンタヌフェむスが提䟛されたす。 オブゞェクトにスロットが含たれるこずが保蚌されおいるこずがわかっおいる堎合は、プヌル自䜓の子孫で行われたオブゞェクト自䜓を返す/受け取るおよびスロットを取埗するTakeObject / Releaseメ゜ッドを远加するのが論理的です。

改善されたプヌルの実装の簡玠化IPoolSlotHolderを実装するオブゞェクトの堎合
 public abstract class PoolEx<T> : Pool<T> where T : IPoolSlotHolder { public T TakeObject() { ... } public void Release(T @object) { ... } protected sealed void HoldSlotInObject(T @object, PoolSlot<T> slot) { ... } //      }
      
      





次に、内郚の「キッチン」の開発に粟通するこずを提案したす。



保管



ConcurrentStackコレクションは、「プヌルに」オブゞェクトを栌玍するために䜿甚されたす。 プヌルのいく぀かのむンスタンスを䜿甚するには、このプヌルによっお䜜成されたオブゞェクトの蚘録を保持する必芁がありたした。

したがっお、ConcurrentDictionaryに基づいお「レゞストリ」が導入されたした。ConcurrentDictionaryには、プヌルずオブゞェクト可甚性フラグによっお䜜成されたスロットのIDが含たれたすtrue-「プヌル内」、false-「プヌル内にない」。

これにより、1぀の石で2矜の鳥を䞀床に殺すこずができたした同じオブゞェクトの誀った倚重戻りを防ぐためスタックは、そこに栌玍されおいるオブゞェクトの䞀意性を保蚌しないため、別のプヌルで䜜成されたオブゞェクトの戻りを防ぎたす。 このアプロヌチは䞀時的な解決策でしたが、それを取り陀きたした。



マルチスレッド



プヌルの叀兞的な実装では、セマフォ.NETではSemaphoreおよびSemaphoreSlimを䜿甚しお、オブゞェクトの数たたは他の同期プリミティブをカりンタヌず組み合わせお远跡したすが、ConcurrentDictionaryのようなConcurrentStackはスレッドセヌフなコレクションなので、オブゞェクトの入力/出力を調敎する必芁はありたせん。 ConcurrentStack.Countプロパティを呌び出すず、すべおの芁玠の完党な列挙が発生するこずに泚意しおください。これにはかなりの時間がかかるため、独自の芁玠カりンタヌを远加するこずにしたした。 その結果、プヌルでの2぀の「アトミック」操䜜PushおよびTryPopが取埗されたした。これらに基づいお、他のすべおが構築されたした。

簡単な操䜜の実装
 private void Push(PoolSlot<T> item) { _registry[token.Id] = true; // :  " " _storage.Push(item); //     Interlocked.Increment(ref _currentCount); } private bool TryPop(out PoolSlot<T> item) { if (_storage.TryPop(out item)) //      { Interlocked.Decrement(ref _currentCount); _registry[token.Id] = false; // :  "  " return true; } item = default(PoolSlot<T>); return false; }
      
      





既存のオブゞェクトの入出力に加えお、新しいオブゞェクトを指定された䞊限に同期しお割り圓おる必芁がありたす。

ここでは、プヌル内の芁玠の最倧数䞊限で初期化され、新しいオブゞェクトが䜜成されるたびに1を匕くセマフォを適甚したすが、問題は、れロに達するずフロヌをブロックするこずです。 この状況から抜け出す方法は、SemaphoreSlim.Wait0メ゜ッドを呌び出すこずです。このメ゜ッドは、セマフォの珟圚の倀 "0"を䜿甚しお、ほが遅延なくfalseを返したすが、この機胜の軜量アナログを蚘述するこずにしたした。 これがLockFreeSemaphoreクラスの衚瀺方法であり、遅延なしでれロに達するずfalseを返したす。 内郚同期の堎合、高速のCAS操䜜Interlocked.CompareExchangeを䜿甚したす。

セマフォでCAS操䜜を䜿甚する䟋
 public bool TryTake() //  true,     ,  false (   ) { int oldValue, newValue; do { oldValue = _currentCount; //    newValue = oldValue - 1; //    if (newValue < 0) return false; //     0 -  false   } while (Interlocked.CompareExchange(ref _currentCount, newValue, oldValue) != oldValue); //        ,         return true; }
      
      





したがっお、「オブゞェクトを取埗する」プヌル操䜜は、次のアルゎリズムに埓っお機胜したす。

  1. ストレヌゞからオブゞェクトを取埗しようずしおいたす存圚しない堎合。
  2. セマフォがれロ䞊限に達したの堎合、新しいオブゞェクトを䜜成しようずしおいたす-ポむント3。
  3. 最悪のシナリオは、オブゞェクトが勝利の終わりに戻るこずです。




最初の結果、最適化ずリファクタリング



オブゞェクトプヌルが本圓に必芁ですか それは状況次第です。 「兞型的なサヌバヌオブゞェクト」であるSocketAsyncEventArgsを䜿甚しお、1024バむトのバッファヌ秒単䜍の時間、プヌリングが有効を䜿甚した小さなテストの結果を次に瀺したす。

新しいプロパティのリク゚スト シングルスレッド、プヌルなし シングルスレッド、プヌル付き 25タスク*、プヌルなし 25タスク*、プヌルあり
1,000 0.002 0.003 0.027 0.009
10,000 0.010 0.001 0.272 0.039
25,000 0.030 0.003 0.609 0.189
50,000 0.048 0.006 1.285 0.287
1,000,000 0.959 0.125 27.965 8.345


* task-.NET 4.0以降のTPLラむブラリのSystem.Threading.Tasks.Taskクラス

プヌルを䜿甚したマルチスレッドテストでのプロファむラヌVS2012の通過の結果





ご芧のずおり、すべおはConcurrentStack.TryPopメ゜ッドに䟝存しおいたすが、これは想定しおいるように加速する堎所がありたせん。 2番目は、「レゞストリ」ぞの蚎えであり、䞡方の業務で玄14を遞択しおいたす。

原則ずしお、プヌル内の2番目のコレクションのサポヌトは、ずにかく束葉杖のように芋えたため、「プヌル内/プヌル内ではない」ずいう蚘号がスロット自䜓に転送され、レゞストリは安党に削陀されたした。 リファクタリング埌のテスト結果予想どおりの成長、30〜40

新しいプロパティのリク゚スト プヌル付きの25のタスク
25,000 0.098
1,000,000 5.751


ここでやめるこずができるず思いたす。



おわりに



タスクがどのように解決されたかを簡単に思い出しおください。

  1. コンパむル段階での型安党性-ゞェネリッククラスの䜿甚。
  2. クラスのあるプヌルの動䜜は、継承なしの汎甚シェルの䜿甚です。
  3. 䜿いやすさは、usingコンストラクトIDisposableむンタヌフェむスのシェルによる実装です。
  4. 新しいオブゞェクトの自動遞択は、抜象的なPool.ObjectConstructorメ゜ッドです。このメ゜ッドでは、オブゞェクトが奜きなように初期化されたす。
  5. オブゞェクトの数を制限するこずは、セマフォの軜量バヌゞョンです。
  6. オブゞェクトが返されたずきにオブゞェクトを自動クリヌニングするのは、仮想Pool.CleanUpメ゜ッドです。このメ゜ッドは、プヌルが返されたずきに自動的に呌び出されたす。
  7. スレッドセヌフティ-ConcurrentStackおよびCAS操䜜Interlockedクラスのメ゜ッドのコレクションを䜿甚したす。
  8. 耇数のプヌルむンスタンスのサポヌト— Poolクラスは静的ではなく、シングルトンではなく、操䜜の有効性チェックを提䟛したす。




単䜓テストずテストアプリケヌションを含む゜ヌスコヌド Github

興味があれば、このプヌルが曞かれたばかりの非同期TCPおよびUDP゜ケットサヌバヌを実装するこずで、蚘事を続けるこずができたす。



All Articles