留在队列中的项目的替代方案(线程消费者 - 生产者)

时间:2015-09-11 19:08:47

标签: c# multithreading queue async-await task-parallel-library

作为使用this code的库的一部分,有一个SimpleQueue类可以将生产者与消费者分开:

private class SimpleQueue
{
    private readonly Func<ResolvedEvent, CancellationToken, Task> _onResolvedEvent;
    private readonly CancellationToken _token;
    private readonly ConcurrentQueue<ResolvedEvent> _events;
    private readonly InterlockedBoolean _isPushing;
    private static readonly ILog s_logger;

    static SimpleQueue()
    {
        s_logger = LogProvider.For<SimpleQueue>();
    }

    public SimpleQueue(Func<ResolvedEvent, CancellationToken, Task> onResolvedEvent, CancellationToken token)
    {
        _onResolvedEvent = onResolvedEvent;
        _token = token;
        _events = new ConcurrentQueue<ResolvedEvent>();
        _isPushing = new InterlockedBoolean();
    }

    public void Enqueue(ResolvedEvent resolvedEvent)
    {
        _events.Enqueue(resolvedEvent);
        Push();
    }

    private void Push()
    {
        if(_isPushing.CompareExchange(true, false))
        {
            return;
        }
        Task.Run(async () =>
        {
            ResolvedEvent resolvedEvent;
            while (!_token.IsCancellationRequested && _events.TryDequeue(out resolvedEvent))
            {
                try
                {
                    await _onResolvedEvent(resolvedEvent, _token);
                }
                catch(Exception ex)
                {
                    s_logger.ErrorException(ex.Message, ex);
                }
            }
            _isPushing.Set(false);
        }, _token);
    }
}

我想我可以在这里看到一个问题,如果:

  1. 在任务主题中,来电events.TryDequeue(out resolvedEvent))会返回false
  2. 然后上下文切换到另一个线程
  3. 在另一个线程事件入队时,Push()被调用,但在_isPushingtrue时立即返回
  4. 上下文切换回任务线程,_isPushing设置为false,任务退出
  5. 在这种情况下,队列中的事件将被调度,直到下一个入队并在Push()中循环出队。如果是这样,我认为我不喜欢这个。

    所以我改写使用TPL BlockingQueue:

    public class SimpleQueue<T>
    {
        readonly BufferBlock<T> _queue = new BufferBlock<T>();
    
        public SimpleQueue(Func<T, CancellationToken, Task> onItemQueued, CancellationToken token)
        {
            Task.Run(async () =>
            {
                while (true)
                {
                    try
                    {
                        var item = await _queue.ReceiveAsync(token);
                        if (token.IsCancellationRequested)
                            return;
                        await onItemQueued(item, token);
                    }
                    catch (Exception ex)
                    {
                        // log
                    }
                }
            }, token);
        }
    
        public void Enqueue(T item)
        {
            _queue.Post(item);
        }
    }
    
    class Program
    {
        private readonly static SimpleQueue<string> Queue;
        private readonly static CancellationToken CancellationToken = new CancellationToken();
    
        static async Task OnEvent(string item, CancellationToken cancellationToken)
        {
            await Task.Run(() =>
            {
                Console.WriteLine("Rx from remote {0}", item);
    
            }, cancellationToken);
        }
    
        static Program()
        {
            Queue = new SimpleQueue<string>(OnEvent, CancellationToken);
        }
    
        static void Main(string[] args)
        {
            // wire up code to call ExternalReceive from 3rd party lib
            DevLinkImports.DLRegisterType2CallDeltas(0,CallEvent);
    
            Console.ReadLine();
        }
    
        // this is called by 3rd party dll on demand
        static void CallEvent(uint pbxh, string info)
        {
            // we must dispatch and return within 50ms or 3rd party lib will go ape
            Queue.Enqueue(info);  
        }
    

    问题:

    1. 出于学习目的,我在查看原始SimpleQueue的问题时是否正确,可以根据时间选择保留项目?

    2. 如果没有“过早优化”,我觉得有必要问一下,为static async Task OnEvent(string item, CancellationToken cancellationToken)的每次调用启动新线程的开销是多少?

    3. 通过重写,我在休眠时不会保持线程打开,但实际上使用此异步调用有任何好处,或者只是改为使用BlockingCollection并在出列时阻塞?我不想保存一个线程来牺牲启动新线程所花费的时间。

1 个答案:

答案 0 :(得分:2)

  
      
  1. 出于学习目的,我在查看原始SimpleQueue的问题时是否正确,可以根据时间选择项目?
  2.   

无法肯定地说,因为此处未提供InterlockedBoolean的实施。您的关注点似乎有效,但在尝试做出明确的陈述之前,我希望看到实际的代码。

  
      
  1. 如果没有“过早优化”,我觉得唯一明智的做法是,为每次调用静态异步任务OnEvent(字符串项,CancellationToken cancellationToken)调整新线程的开销是多少?
  2.   

创建线程的开销很大。但是您的OnEvent()方法可能会或可能不会实际执行此操作。您正在创建一个新任务,然后调度程序将决定如何处置它。如果线程池包含用于执行它的可用线程和/或调度程序决定它可以等待现有但很繁忙的线程变为可用,则不会创建新线程。

  
      
  1. 使用重写我睡觉时不会保持线程打开,但实际上使用这个异步调用有什么好处,或者只是旋转一个线程并使用BlockingCollection并阻塞出队?我不想保存一个线程来牺牲启动新线程所花费的时间。
  2.   

在您的程序中添加单个线程来为队列提供服务并不是那么糟糕。你只需创建一次,因此开销无关紧要。它确实为堆栈占用了一兆字节(默认情况下),但这通常也不会成为问题。

另一方面,由于使用了线程池,同样调用Task.Run()也不太可能导致大量开销。

所以对我来说,它归结为美学和可维护性。

我要指出,使用BlockingCollection<T> vs BufferBlock<T>的问题与OnEvent()的实施问题有些不同。前者涉及底层队列的实现,而后者涉及事件实际上已经出列时发生的事情。即使您使用BlockingCollection<T>,如果您不更改OnEvent(),您仍然会为每个事件开始新任务。相反,即使使用OnEvent(),也无法让BufferBlock<T>同步运行事件处理。

队列代码显然期望异步处理事件,但事件不一定如此。这取决于队列的客户端。