awaitable Task based queue

前端 未结 9 1107
礼貌的吻别
礼貌的吻别 2020-11-27 14:44

I\'m wondering if there exists an implementation/wrapper for ConcurrentQueue, similar to BlockingCollection where taking from the collection does not block, but is instead a

相关标签:
9条回答
  • 2020-11-27 15:26

    It may be overkill for your use case (given the learning curve), but Reactive Extentions provides all the glue you could ever want for asynchronous composition.

    You essentially subscribe to changes and they are pushed to you as they become available, and you can have the system push the changes on a separate thread.

    0 讨论(0)
  • 2020-11-27 15:30

    Here's the implementation I'm currently using.

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

    It works good enough, but there's quite a lot of contention on queueSyncLock, as I am making quite a lot of use of the CancellationToken to cancel some of the waiting tasks. Of course, this leads to considerably less blocking I would see with a BlockingCollection but...

    I'm wondering if there is a smoother, lock free means of achieving the same end

    0 讨论(0)
  • 2020-11-27 15:32

    My atempt (it have an event raised when a "promise" is created, and it can be used by an external producer to know when to produce more items):

    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;
    }
    
    0 讨论(0)
  • 2020-11-27 15:34

    Check out https://github.com/somdoron/AsyncCollection, you can both dequeue asynchronously and use C# 8.0 IAsyncEnumerable.

    The API is very similar to 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();
    

    With 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();
    
    0 讨论(0)
  • 2020-11-27 15:39

    I don't know of a lock-free solution, but you can take a look at the new Dataflow library, part of the Async CTP. A simple BufferBlock<T> should suffice, e.g.:

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

    Production and consumption are most easily done via extension methods on the dataflow block types.

    Production is as simple as:

    buffer.Post(13);
    

    and consumption is async-ready:

    int item = await buffer.ReceiveAsync();
    

    I do recommend you use Dataflow if possible; making such a buffer both efficient and correct is more difficult than it first appears.

    0 讨论(0)
  • 2020-11-27 15:41

    One simple and easy way to implement this is with a 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(TimeSpan timeSpan, CancellationToken cancellationToken)
        {
            await semaphore.WaitAsync(timeSpan, cancellationToken);
            lock (queueLock)
            {
                return queue.Dequeue();
            }
        }
    }
    

    The beauty of this is that the SemaphoreSlim handles all of the complexity of implementing the Wait() and WaitAsync() functionality. The downside is that queue length is tracked by both the semaphore and the queue itself, and they both magically stay in sync.

    0 讨论(0)
提交回复
热议问题