web-dev-qa-db-ja.com

待機可能なタスクベースのキュー

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);
        }
    }
}
36
spender

ロックフリーソリューションについては知りませんが、新しい DataflowライブラリAsync CTP の一部をご覧ください。シンプルな BufferBlock<T>で十分です。例:

BufferBlock<int> buffer = new BufferBlock<int>();

生成と消費は、データフローブロックタイプの拡張メソッドを介して最も簡単に実行されます。

生産は次のように簡単です:

buffer.Post(13);

消費は非同期対応です:

int item = await buffer.ReceiveAsync();

可能であればDataflowを使用することをお勧めします。そのようなバッファを効率的かつ正しいものにすることは、最初に現れるよりも困難です。

55
Stephen Cleary

C#8.0 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();
        }

    }
}
17
Bruno Zell

私の試み(「約束」が作成されたときに発生するイベントがあり、外部プロデューサーがそれを使用していつアイテムをさらに作成するかを知ることができます):

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;
}
3
André Bires

ユースケース(学習曲線を考えると)にはやり過ぎかもしれませんが、 Reactive Extentions は、非同期合成に必要なすべての接着剤を提供します。

基本的に変更をサブスクライブし、変更が利用可能になるとプッシュされます。別のスレッドでシステムに変更をプッシュさせることができます。

1
Morten Mertner

チェックアウト 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
somdoron

これを実装する簡単で簡単な方法の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();
        }
    }
}
_

この優れた点は、SemaphoreSlimWait()およびWaitAsync()機能の実装の複雑さをすべて処理できることです。欠点は、キューの長さがセマフォおよびキュー自体の両方で追跡され、両者が魔法のように同期を保つことです。

0
Ryan

さて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 <>で発生していた問題が修正されたことがわかりました。

0
bmiller

これが私が現在使用している実装です。

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で見られるブロッキングをかなり少なくしますが...

同じ目的を達成するためのスムーズでロックフリーの手段があるかどうか私は思っています

0
spender