Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9370

BlockingCollection の使い方

$
0
0

コードリーディングしている時にBlockingCollectionというものが出てきてよくわからなかったので、調べてみました。主にBlockingCollection in C# - Introduction and Examplesを参考にさせていただいて、自分のコードを書いてみました。

Blocking Collection とは

BlockingCollectionはProducer-Consumer パターンの実装です。IPublisherConsumerCollection<T>というインターフェイスがありますが、そのスレッドセーフの実装です。つまり、コンカレントな状況においてもちゃん動いてくれるものです。

デフォルトでは内部的にConcurrentQueueを用いているようですが、コンストラクタでIProducerConsumerCollection<T>を実装したクラスを渡すことで、そちらのクラスを使用するように変えることもできるようです。インターフェイスは次のものになっています。

IProducerConsumerCollection

publicinterfaceIProducerConsumerCollection<T>:IEnumerable<T>,IEnumerable,ICollection{voidCopyTo(T[]array,intindex);T[]ToArray();boolTryAdd(Titem);boolTryTake([MaybeNullWhen(false)]outTitem);}

コンストラクタ

実際に挙動を確認してみましょう。BlockingCollectionクラスを単純にインスタンス生成します。boundedCapacityを渡すことで、このコレクションの最大値を指定することができます。先ほど述べた通り、ここで、ConcurrentQueue以外の実装を渡すことも可能です。

varblockingCollection=newBlockingCollection<string>(boundedCapacity:3);

Producer

コンカレントOKとの事なので、スレッドを生成して、そこで、Addを使って要素を足していきます。ここでは、コンソールから入力したものを渡しています。挙動としてのポイントは、上記のコンストラクタで指定した、boundedCapacityを超えると、Addメソッドがブロックして、Consumer がアイテムを取得してくれるのを待ちます。

TaskproducerThread=Task.Factory.StartNew(()=>{while(true){stringcommand=Console.ReadLine();if(command.Contains("quit"))break;blockingCollection.Add(command);// blocked if it reach the capacity}});

この待ち受けの挙動が好きではない場合、TryAddメソッドもあります。このメソッドの場合、一定の時間ブロックされたら、「失敗した」とみなして処理をさせることも可能です。CancellationTokenを持つオーバーロードもあります。

if(blockingCollection.TryAdd(command,TimeSpan.FromSeconds(1))){// it works!}else{Console.WriteLine($"It reached boundedCapacity: {capacity} couldn't add {command}");}

Consumer

Takeメソッドにより、1件のアイテムを取得することができます。もし、BlockingCollectionのインスタンスに1件もなかったら、ここでブロックされます。blockingCollection.IsCompleteメソッドで、BlockingCollectionが終了したことの通知を受け取ることができます。

NOTE

ちなみにこのサンプルで、.GetAwaiter().GetResult()みたいなクソださなことをしているかというと、Task.Factory.StartNew(async () => {}にすると、async のため labmda の実行がブロックされて、すぐに終了したとみなされて、後に出てくる WaitAllメソッドでこのスレッドの終了を待ち受ける処理がうまく動作しなくなるからです。正直もっといいやり方がありそう。本番では、async/await を使うので直接スレッドを起動していないので、問題にはなっていませんが、、ダサさを何とかしたい。

TaskconsumerAThread=Task.Factory.StartNew(()=>{while(true){if(blockingCollection.IsCompleted)break;stringcommand=blockingCollection.Take();Console.WriteLine($"ConsumerA: Take Received: {command}");Task.Delay(TimeSpan.FromSeconds(10)).GetAwaiter().GetResult();}});

TryTakeメソッドは、ブロック中に、一定の時間がたつと失敗させてくれるメソッドです。

TaskconsumerBThread=Task.Factory.StartNew(()=>{while(true){if(blockingCollection.IsCompleted)break;stringcommand;if(blockingCollection.TryTake(outcommand,TimeSpan.FromSeconds(5))){Console.WriteLine($"ConsumerB: TryTake Received: {command}");}else{Console.WriteLine($"consumerB: Can't take now.");}Task.Delay(TimeSpan.FromSeconds(10)).GetAwaiter().GetResult();}});

CancellationToken

TryXXXメソッドは、CancellationTokenをサポートしているので、活用することもできます。CancellationTokenが発行されたら、OperationCanceledExceptionがスローされます。

CancellationTokenSourcesource=newCancellationTokenSource();
TaskconsumerBThread=Task.Factory.StartNew(()=>{while(true){if(blockingCollection.IsCompleted)break;stringcommand;try{if(blockingCollection.TryTake(outcommand,(int)TimeSpan.FromSeconds(5).TotalMilliseconds,source.Token)){Console.WriteLine($"ConsumerB: TryTake Received: {command}");}else{Console.WriteLine($"consumerB: Can't take now.");}}catch(OperationCanceledExceptione){Console.WriteLine($"ConsumerB: Task is cancelled.: {e.Message}");break;}Task.Delay(TimeSpan.FromSeconds(10)).GetAwaiter().GetResult();}});

プログラムの全体像をシェアしておきます。実行して、何かを入力すると、ProducerがBlockingCollectionにアイテムを代入していきます。Producer , ConsumerA, ConsumerBのブロッキングの振る舞いを観察することができます。cancelとタイプすると、CancellationTokenが発行されて終了します。もしくは、quitで終了します。

classProgram{staticvoidMain(string[]args){intcapacity=3;// Blocking Collection varblockingCollection=newBlockingCollection<string>(boundedCapacity:capacity);CancellationTokenSourcesource=newCancellationTokenSource();TaskproducerThread=Task.Factory.StartNew(()=>{while(true){stringcommand=Console.ReadLine();if(command.Contains("quit"))break;if(command.Contains("cancel")){Console.WriteLine("Cancelling ...");source.Cancel();break;}// blockingCollection.Add(command);   // blocked if it reach the capacityif(blockingCollection.TryAdd(command,TimeSpan.FromSeconds(1))){// it works!}else{Console.WriteLine($"It reached boundedCapacity: {capacity} couldn't add {command}");}}});TaskconsumerAThread=Task.Factory.StartNew(()=>{while(true){if(blockingCollection.IsCompleted)break;stringcommand=blockingCollection.Take();Console.WriteLine($"ConsumerA: Take Received: {command}");Task.Delay(TimeSpan.FromSeconds(10)).GetAwaiter().GetResult();}});TaskconsumerBThread=Task.Factory.StartNew(()=>{while(true){if(blockingCollection.IsCompleted)break;stringcommand;try{if(blockingCollection.TryTake(outcommand,(int)TimeSpan.FromSeconds(5).TotalMilliseconds,source.Token)){Console.WriteLine($"ConsumerB: TryTake Received: {command}");}else{Console.WriteLine($"consumerB: Can't take now.");}}catch(OperationCanceledExceptione){Console.WriteLine($"ConsumerB: Task is cancelled.: {e.Message}");break;}Task.Delay(TimeSpan.FromSeconds(10)).GetAwaiter().GetResult();}});Task.WaitAll(producerThread,consumerAThread,consumerBThread);}}

Source

今回のサンプルです。
* Sample


Viewing all articles
Browse latest Browse all 9370

Latest Images

Trending Articles