ConcurrentQueue の実装/ラッパーは BlockingCollection に似ていますが、コレクションからの取得がブロックされず、非同期で非同期の待機が発生するのではないかと思います。アイテムがキューに配置されるまで。
独自の実装を考え出しましたが、期待どおりに動作していないようです。すでに存在しているものを再発明しているのではないかと思っています。
これが私の実装です:
public class MessageQueue<T>
{
ConcurrentQueue<T> queue = new ConcurrentQueue<T>();
ConcurrentQueue<TaskCompletionSource<T>> waitingQueue =
new ConcurrentQueue<TaskCompletionSource<T>>();
object queueSyncLock = new object();
public void Enqueue(T item)
{
queue.Enqueue(item);
ProcessQueues();
}
public async Task<T> Dequeue()
{
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
waitingQueue.Enqueue(tcs);
ProcessQueues();
return tcs.Task.IsCompleted ? tcs.Task.Result : await tcs.Task;
}
private void ProcessQueues()
{
TaskCompletionSource<T> tcs=null;
T firstItem=default(T);
while (true)
{
bool ok;
lock (queueSyncLock)
{
ok = waitingQueue.TryPeek(out tcs) && queue.TryPeek(out firstItem);
if (ok)
{
waitingQueue.TryDequeue(out tcs);
queue.TryDequeue(out firstItem);
}
}
if (!ok) break;
tcs.SetResult(firstItem);
}
}
}
ロックフリーソリューションについては知りませんが、新しい Dataflowライブラリ 、 Async CTP の一部をご覧ください。シンプルな BufferBlock<T>
で十分です。例:
BufferBlock<int> buffer = new BufferBlock<int>();
生成と消費は、データフローブロックタイプの拡張メソッドを介して最も簡単に実行されます。
生産は次のように簡単です:
buffer.Post(13);
消費は非同期対応です:
int item = await buffer.ReceiveAsync();
可能であればDataflowを使用することをお勧めします。そのようなバッファを効率的かつ正しいものにすることは、最初に現れるよりも困難です。
IAsyncEnumerable
および Dataflowライブラリ を使用した簡単なアプローチ// Instatiate an async queue
var queue = new AsyncQueue<int>();
// Then, loop through the elements of queue.
// This loop won't stop until it is canceled or broken out of
// (for that, use queue.WithCancellation(..) or break;)
await foreach(int i in queue) {
// Writes a line as soon as some other Task calls queue.Enqueue(..)
Console.WriteLine(i);
}
次のようにAsyncQueue
を実装すると、
public class AsyncQueue<T> : IAsyncEnumerable<T>
{
private readonly SemaphoreSlim _enumerationSemaphore = new SemaphoreSlim(1);
private readonly BufferBlock<T> _bufferBlock = new BufferBlock<T>();
public void Enqueue(T item) =>
_bufferBlock.Post(item);
public async IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken token = default)
{
// We lock this so we only ever enumerate once at a time.
// That way we ensure all items are returned in a continuous
// fashion with no 'holes' in the data when two foreach compete.
await _enumerationSemaphore.WaitAsync();
try {
// Return new elements until cancellationToken is triggered.
while (true) {
// Make sure to throw on cancellation so the Task will transfer into a canceled state
token.ThrowIfCancellationRequested();
yield return await _bufferBlock.ReceiveAsync(token);
}
} finally {
_enumerationSemaphore.Release();
}
}
}
私の試み(「約束」が作成されたときに発生するイベントがあり、外部プロデューサーがそれを使用していつアイテムをさらに作成するかを知ることができます):
public class AsyncQueue<T>
{
private ConcurrentQueue<T> _bufferQueue;
private ConcurrentQueue<TaskCompletionSource<T>> _promisesQueue;
private object _syncRoot = new object();
public AsyncQueue()
{
_bufferQueue = new ConcurrentQueue<T>();
_promisesQueue = new ConcurrentQueue<TaskCompletionSource<T>>();
}
/// <summary>
/// Enqueues the specified item.
/// </summary>
/// <param name="item">The item.</param>
public void Enqueue(T item)
{
TaskCompletionSource<T> promise;
do
{
if (_promisesQueue.TryDequeue(out promise) &&
!promise.Task.IsCanceled &&
promise.TrySetResult(item))
{
return;
}
}
while (promise != null);
lock (_syncRoot)
{
if (_promisesQueue.TryDequeue(out promise) &&
!promise.Task.IsCanceled &&
promise.TrySetResult(item))
{
return;
}
_bufferQueue.Enqueue(item);
}
}
/// <summary>
/// Dequeues the asynchronous.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns></returns>
public Task<T> DequeueAsync(CancellationToken cancellationToken)
{
T item;
if (!_bufferQueue.TryDequeue(out item))
{
lock (_syncRoot)
{
if (!_bufferQueue.TryDequeue(out item))
{
var promise = new TaskCompletionSource<T>();
cancellationToken.Register(() => promise.TrySetCanceled());
_promisesQueue.Enqueue(promise);
this.PromiseAdded.RaiseEvent(this, EventArgs.Empty);
return promise.Task;
}
}
}
return Task.FromResult(item);
}
/// <summary>
/// Gets a value indicating whether this instance has promises.
/// </summary>
/// <value>
/// <c>true</c> if this instance has promises; otherwise, <c>false</c>.
/// </value>
public bool HasPromises
{
get { return _promisesQueue.Where(p => !p.Task.IsCanceled).Count() > 0; }
}
/// <summary>
/// Occurs when a new promise
/// is generated by the queue
/// </summary>
public event EventHandler PromiseAdded;
}
ユースケース(学習曲線を考えると)にはやり過ぎかもしれませんが、 Reactive Extentions は、非同期合成に必要なすべての接着剤を提供します。
基本的に変更をサブスクライブし、変更が利用可能になるとプッシュされます。別のスレッドでシステムに変更をプッシュさせることができます。
チェックアウト https://github.com/somdoron/AsyncCollection 、非同期でデキューすることも、C#8.0 IAsyncEnumerableを使用することもできます。
APIはBlockingCollectionによく似ています。
AsyncCollection<int> collection = new AsyncCollection<int>();
var t = Task.Run(async () =>
{
while (!collection.IsCompleted)
{
var item = await collection.TakeAsync();
// process
}
});
for (int i = 0; i < 1000; i++)
{
collection.Add(i);
}
collection.CompleteAdding();
t.Wait();
IAsyncEnumeableの場合:
AsyncCollection<int> collection = new AsyncCollection<int>();
var t = Task.Run(async () =>
{
await foreach (var item in collection)
{
// process
}
});
for (int i = 0; i < 1000; i++)
{
collection.Add(i);
}
collection.CompleteAdding();
t.Wait();
これを実装する簡単で簡単な方法の1つは、SemaphoreSlim
を使用することです。
_public class AwaitableQueue<T>
{
private SemaphoreSlim semaphore = new SemaphoreSlim(0);
private readonly object queueLock = new object();
private Queue<T> queue = new Queue<T>();
public void Enqueue(T item)
{
lock (queueLock)
{
queue.Enqueue(item);
semaphore.Release();
}
}
public T WaitAndDequeue(TimeSpan timeSpan, CancellationToken cancellationToken)
{
semaphore.Wait(timeSpan, cancellationToken);
lock (queueLock)
{
return queue.Dequeue();
}
}
public async Task<T> WhenDequeue()
{
await semaphore.WaitAsync(TimeSpan timeSpan, CancellationToken cancellationToken);
lock (queueLock)
{
return queue.Dequeue();
}
}
}
_
この優れた点は、SemaphoreSlim
がWait()
およびWaitAsync()
機能の実装の複雑さをすべて処理できることです。欠点は、キューの長さがセマフォおよびキュー自体の両方で追跡され、両者が魔法のように同期を保つことです。
さて8年後、私はまさにこの質問にぶつかり、MSを実装しようとしていましたAsyncQueue<T>
nugetパッケージ/名前空間でクラスが見つかりました:Microsoft.VisualStudio.Threading
このAPIについて言及してくれた@Theodor Zouliasに感謝します。
そこで、AsyncQueue <>実装を編集して、BufferBlock <>を使用しました。ほとんど同じですが、うまく機能します。
これをAspNet Coreバックグラウンドスレッドで使用すると、完全に非同期で実行されます。
protected async Task MyRun()
{
BufferBlock<MyObj> queue = new BufferBlock<MyObj>();
Task enqueueTask = StartDataIteration(queue);
while (await queue.OutputAvailableAsync())
{
var myObj = queue.Receive();
// do something with myObj
}
}
public async Task StartDataIteration(BufferBlock<MyObj> queue)
{
var cursor = await RunQuery();
while(await cursor.Next()) {
queue.Post(cursor.Current);
}
queue.Complete(); // <<< signals the consumer when queue.Count reaches 0
}
Queue.OutputAvailableAsync()を使用すると、AsyncQueue <>で発生していた問題が修正されたことがわかりました。
これが私が現在使用している実装です。
public class MessageQueue<T>
{
ConcurrentQueue<T> queue = new ConcurrentQueue<T>();
ConcurrentQueue<TaskCompletionSource<T>> waitingQueue =
new ConcurrentQueue<TaskCompletionSource<T>>();
object queueSyncLock = new object();
public void Enqueue(T item)
{
queue.Enqueue(item);
ProcessQueues();
}
public async Task<T> DequeueAsync(CancellationToken ct)
{
TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
ct.Register(() =>
{
lock (queueSyncLock)
{
tcs.TrySetCanceled();
}
});
waitingQueue.Enqueue(tcs);
ProcessQueues();
return tcs.Task.IsCompleted ? tcs.Task.Result : await tcs.Task;
}
private void ProcessQueues()
{
TaskCompletionSource<T> tcs = null;
T firstItem = default(T);
lock (queueSyncLock)
{
while (true)
{
if (waitingQueue.TryPeek(out tcs) && queue.TryPeek(out firstItem))
{
waitingQueue.TryDequeue(out tcs);
if (tcs.Task.IsCanceled)
{
continue;
}
queue.TryDequeue(out firstItem);
}
else
{
break;
}
tcs.SetResult(firstItem);
}
}
}
}
十分に機能しますが、queueSyncLock
を使用して待機中のタスクの一部をキャンセルしているため、CancellationToken
にはかなりの競合があります。もちろん、これはBlockingCollection
で見られるブロッキングをかなり少なくしますが...
同じ目的を達成するためのスムーズでロックフリーの手段があるかどうか私は思っています