繁体   English   中英

从生产者/消费者队列中删除已取消的任务

[英]Remove cancelled Task from producer/consumer queue

我想使用异步生产者/消费者队列(AsyncEx lib)一次通过总线发送一条消息。 现在,我仅通过异步阻止来实现此目的。 它工作正常,但是我无法控制队列:(

因此,我想出了以下解决方案,问题是未从队列中删除已取消的任务。 如果我将队列的大小限制为10(因为每个消息的发送时间为1s,最大队列时间为10s左右),并且队列中已经包含8个等待的任务和2个已取消的任务,则下一个排队的任务将抛出InvalidOperationException,尽管无论如何,两个已取消的任务将不会发送。

也许有更好的方法可以做到这一点:D

    class Program
{
    static AsyncProducerConsumerQueue<Tuple<string, TaskCompletionSource>> s_Queue =
        new AsyncProducerConsumerQueue<Tuple<string, TaskCompletionSource>>();

    static void Main()
    {
        StartAsync().Wait();
    }

    static async Task StartAsync()
    {
        var sendingTask = StartSendingAsync();
        var tasks = new List<Task>();

        using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8)))
        {
            for (var i = 0; i < 10; i++)
            {
                tasks.Add(EnqueueMessageAsync("Message " + i, cts.Token));
            }

            try
            {
                await Task.WhenAll(tasks);
                Console.WriteLine("All messages sent.");
            }
            catch (TaskCanceledException)
            {
                Console.WriteLine("At least one task was canceled.");
            }                
        }

        s_Queue.CompleteAdding();
        await sendingTask;
        s_Queue.Dispose();
        Console.WriteLine("Queue completed.");

        Console.WriteLine("Press any key to continue...");
        Console.ReadKey();
    }

    static async Task EnqueueMessageAsync(string message, CancellationToken token)
    {

        var tcs = new TaskCompletionSource();
        using (token.Register(() => tcs.TrySetCanceled()))
        {
            await s_Queue.EnqueueAsync(new Tuple<string, TaskCompletionSource>(message, tcs));
            Console.WriteLine("Thread '{0}' - {1}: {2} queued.", Thread.CurrentThread.ManagedThreadId, DateTime.Now.TimeOfDay, message);
            await tcs.Task;
        }
    }

    static async Task SendMessageAsync(string message)
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        Console.WriteLine("Thread '{0}' - {1}: {2} sent.", Thread.CurrentThread.ManagedThreadId, DateTime.Now.TimeOfDay, message);
    }

    static async Task StartSendingAsync()
    {
        while (await s_Queue.OutputAvailableAsync())
        {
            var t = await s_Queue.DequeueAsync();
            if (t.Item2.Task.IsCanceled || t.Item2.Task.IsFaulted) continue;

            await SendMessageAsync(t.Item1);
            t.Item2.TrySetResult();
        }
    }
}

编辑1:

如svik所指出的,仅当队列已完成时才引发InvalidOperationException。 因此,该解决方案甚至无法解决我的最初问题,即等待任务的不受管理的“队列”。 例如,如果有10个以上的呼叫/ 10s,我将有一个完整的队列和一个等待任务的其他非托管“队列”,例如异步阻塞方法(AsyncMonitor)。 我想我必须再提出其他解决方案...

编辑2:

我有N个不同的消息产生者(我不知道有多少个消息产生者,因为这不是我的代码),只有一个使用者通过总线发送消息并检查消息是否正确发送(不是真正的字符串消息)。

下面的代码模拟了代码应中断的情况(队列大小为10):

  1. 使10条消息入队(超时为5秒)
  2. 等待5秒(发送了消息0-4,并且取消了消息5-9)
  3. 使11条新消息入队(无超时)
  4. 消息10-19应该入队,因为队列仅包含已取消的消息
  5. 消息20应该引发异常(例如QueueOverflowException),因为队列已满,生产者代码可以处理或不处理

制片人:

using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
{
    for (var i = 0; i < 10; i++) { tasks.Add(EnqueueMessageAsync("Message " + i, cts.Token)); }
    await Task.Delay(TimeSpan.FromSeconds(5));
    for (var i = 10; i < 21; i++) { tasks.Add(EnqueueMessageAsync("Message " + i, default(CancellationToken))); }

    try
    {
        await Task.WhenAll(tasks);
        Console.WriteLine("All messages sent.");
    }
    catch (TaskCanceledException)
    {
        Console.WriteLine("At least one task was canceled.");
        Console.WriteLine("Press any key to complete queue...");
        Console.ReadKey();
    }
}

目标是,我想完全控制应该发送的所有消息,但是在我之前发布的代码中却不是这种情况,因为我只能控制队列中的消息,而不能控制队列中的消息。等待入队(可能有10000条消息异步等待入队,我不知道=>生产者代码无论如何都无法按预期工作,因为要发送所有等待中的消息会花费很多时间...)

我希望这可以使我想要实现的目标更加清晰;)

我不确定回答自己的问题是否可以,所以我不会将其标记为答案,也许有人会提出更好的解决方案:P

首先是生产者代码:

static async Task StartAsync()
{
    using (var queue = new SendMessageQueue(10, new SendMessageService()))
    using (var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(4.5)))
    {
        var tasks = new List<Task>();

        for (var i = 0; i < 10; i++)
        {
            tasks.Add(queue.SendAsync(i.ToString(), timeoutTokenSource.Token));
        }
        await Task.Delay(TimeSpan.FromSeconds(4.5));
        for (var i = 10; i < 25; i++)
        {
            tasks.Add(queue.SendAsync(i.ToString(), default(CancellationToken)));
        }

        await queue.CompleteSendingAsync();

        for (var i = 0; i < tasks.Count; i++ )
        {
            try
            {
                await tasks[i];
                Console.WriteLine("Message '{0}' send.", i);
            }
            catch (TaskCanceledException)
            {
                Console.WriteLine("Message '{0}' canceled.", i);
            }
            catch (QueueOverflowException ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
    Console.WriteLine("Press any key to continue...");
    Console.ReadKey();
}
  • 5秒内将25条消息放入队列
  • 已发送16条消息
  • 3条消息未发送(队列已满)
  • 6条消息被取消

这是基于列表的“队列”类。 它是队列和使用者的结合。 同步是通过AsyncMonitor类(Stephen Cleary的AsyncEx)完成的。

class SendMessageQueue : IDisposable
{
    private bool m_Disposed;
    private bool m_CompleteSending;
    private Task m_SendingTask;
    private AsyncMonitor m_Monitor;
    private List<MessageTaskCompletionSource> m_MessageCollection;
    private ISendMessageService m_SendMessageService;

    public int Capacity { get; private set; }


    public SendMessageQueue(int capacity, ISendMessageService service)
    {
        Capacity = capacity;
        m_Monitor = new AsyncMonitor();
        m_MessageCollection = new List<MessageTaskCompletionSource>();
        m_SendMessageService = service;
        m_SendingTask = StartSendingAsync();
    }

    public async Task<bool> SendAsync(string message, CancellationToken token)
    {
        if (m_Disposed) { throw new ObjectDisposedException(GetType().Name); }
        if (message == null) { throw new ArgumentNullException("message"); }

        using (var messageTcs = new MessageTaskCompletionSource(message, token))
        {
            await AddAsync(messageTcs);
            return await messageTcs.Task;
        }
    }

    public async Task CompleteSendingAsync()
    {
        if (m_Disposed) { throw new ObjectDisposedException(GetType().Name); }

        using (m_Monitor.Enter())
        {
            m_CompleteSending = true;
        }
        await m_SendingTask;
    }

    private async Task AddAsync(MessageTaskCompletionSource message)
    {
        using (await m_Monitor.EnterAsync(message.Token))
        {
            if (m_CompleteSending) { throw new InvalidOperationException("Queue already completed."); }
            if (Capacity < m_MessageCollection.Count)
            {
                m_MessageCollection.RemoveAll(item => item.IsCanceled);
                if (Capacity < m_MessageCollection.Count)
                {
                    throw new QueueOverflowException(string.Format("Queue overflow; '{0}' couldn't be enqueued.", message.Message));
                }
            }
            m_MessageCollection.Add(message);
        }
        m_Monitor.Pulse(); // signal new message
        Console.WriteLine("Thread '{0}' - {1}: '{2}' enqueued.", Thread.CurrentThread.ManagedThreadId, DateTime.Now.TimeOfDay, message.Message);
    }

    private async Task<MessageTaskCompletionSource> TakeAsync()
    {
        using (await m_Monitor.EnterAsync())
        {
            var message = m_MessageCollection.ElementAt(0);
            m_MessageCollection.RemoveAt(0);
            return message;
        }
    }

    private async Task<bool> OutputAvailableAsync()
    {
        using (await m_Monitor.EnterAsync())
        {
            if (m_MessageCollection.Count > 0) { return true; }
            else if (m_CompleteSending) { return false; }

            await m_Monitor.WaitAsync();
            return true;
        }
    }

    private async Task StartSendingAsync()
    {
        while (await OutputAvailableAsync())
        {
            var message = await TakeAsync();
            if (message.IsCanceled) continue;
            try
            {
                var result = await m_SendMessageService.SendMessageAsync(message.Message, message.Token);
                message.TrySetResult(result);
            }
            catch (TaskCanceledException) { message.TrySetCanceled(); }
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected void Dispose(bool disposing)
    {
        if (m_Disposed) return;
        if (disposing)
        {
            if (m_MessageCollection != null)
            {
                var tmp = m_MessageCollection;
                m_MessageCollection = null;
                tmp.ForEach(item => item.Dispose());
                tmp.Clear();
            }
        }
        m_Disposed = true;
    }

    #region MessageTaskCompletionSource Class

    class MessageTaskCompletionSource : TaskCompletionSource<bool>, IDisposable
    {
        private bool m_Disposed;
        private IDisposable m_CancellationTokenRegistration;

        public string Message { get; private set; }
        public CancellationToken Token { get; private set; }
        public bool IsCompleted { get { return Task.IsCompleted; } }
        public bool IsCanceled { get { return Task.IsCanceled; } }
        public bool IsFaulted { get { return Task.IsFaulted; } }


        public MessageTaskCompletionSource(string message, CancellationToken token)
        {
            m_CancellationTokenRegistration = token.Register(() => TrySetCanceled());
            Message = message;
            Token = token;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected void Dispose(bool disposing)
        {
            if (m_Disposed) return;
            if (disposing)
            {
                TrySetException(new ObjectDisposedException(GetType().Name));

                if (m_CancellationTokenRegistration != null)
                {
                    var tmp = m_CancellationTokenRegistration;
                    m_CancellationTokenRegistration = null;
                    tmp.Dispose();
                }
            }
            m_Disposed = true;
        }
    }

    #endregion
}

现在我可以接受这种解决方案了; 它完成了工作:D

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM