【问题标题】:Asynchronous Take from blocking collection从阻塞集合中异步获取
【发布时间】:2012-08-14 09:27:33
【问题描述】:

我正在使用BlockingCollection 来实现生产者/消费者模式。我有一个异步循环,它用要处理的数据填充集合,然后客户端可以在以后访问这些数据。数据包很少到达,我希望在不使用阻塞调用的情况下完成轮询。

本质上,我正在寻找阻塞集合中不存在的BeginTakeEndTake 之类的东西,以便我可以在回调中使用内部线程池。无论如何,它不一定是BlockingCollection。任何能满足我需要的东西都会很棒。

这就是我现在所拥有的。 _bufferedPacketsBlockingCollection<byte[]>

public byte[] Read(int timeout)
{
    byte[] result;
    if (_bufferedPackets.IsCompleted)
    {
        throw new Exception("Out of packets");
    }
    _bufferedPackets.TryTake(out result, timeout);      
    return result;
}

我希望它是这样的,在伪代码中:

public void Read(int timeout)
{
    _bufferedPackets.BeginTake(result =>
        {
            var bytes = _bufferedPackets.EndTake(result);
            // Process the bytes, or the resuting timeout
        }, timeout, _bufferedPackets);
}

对此我有什么选择?我不想将 任何 线程置于等待状态,因为还有很多其他 IO 内容要处理,而且我很快就会用完线程。

更新:我已经重写了相关代码,以不同方式使用异步进程,本质上是根据超时限制内是否存在等待请求来交换回调。这工作得很好,但如果有一种方法可以做到这一点,而无需求助于计时器和交换可能导致竞争条件并且难以编写(和理解)的 lambdas,它仍然会很棒。我也通过自己的异步队列实现解决了这个问题,但如果有一个更标准且经过良好测试的选项,它仍然会很棒。

【问题讨论】:

  • 目前,我认为没有任何 TPL 集合向 UI 提供除 ObservableCollection 之外的异步方法。你怎么看?
  • 您可以将其包装在 Task<byte[]> task = Task.Factory.StartNew<byte[]>(() => { // Your Code returning byte[] }); 中,但这并不明智,必须有更好的方法...
  • 包装一个任务将消耗一个任务,该任务将被锁定在等待句柄中。由于有许多任务正在进行,这将永远占用一个任务,这将使我不幸地用完池中的任务。
  • @Dervall 如果您担心会达到池中的最大线程数,那么我认为您的设计有问题。也许BlockingCollection.TakeFromAny() 可能有用?
  • Stephen Toub 描述了在这里编写生产者/生产者队列:blogs.msdn.com/b/toub/archive/2006/04/12/blocking-queues.aspx 但我认为编写 BeginDequeue 或 DequeueAsync 将是微不足道的......

标签: c# asynchronous task-parallel-library


【解决方案1】:

我可能误解了你的情况,但你不能使用非阻塞集合吗?

我创建了这个例子来说明:

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncTakeFromBlockingCollection
{
    class Program
    {
        static void Main(string[] args)
        {
            var queue = new ConcurrentQueue<string>();

            var producer1 = Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 10; i += 1)
                {
                    queue.Enqueue("=======");
                    Thread.Sleep(10);
                }
            });

            var producer2 = Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 10; i += 1)
                {
                    queue.Enqueue("*******");
                    Thread.Sleep(3);
                }
            });

            CreateConsumerTask("One  ", 3, queue);
            CreateConsumerTask("Two  ", 4, queue);
            CreateConsumerTask("Three", 7, queue);

            producer1.Wait();
            producer2.Wait();
            Console.WriteLine("  Producers Finished");
            Console.ReadLine();
        }

        static void CreateConsumerTask(string taskName, int sleepTime, ConcurrentQueue<string> queue)
        {
            Task.Factory.StartNew(() =>
            {
                while (true)
                {
                    string result;
                    if (queue.TryDequeue(out result))
                    {
                        Console.WriteLine("  {0} consumed {1}", taskName, result);
                    }
                    Thread.Sleep(sleepTime);
                }
            });
        }
    }
}

这是程序的输出

我相信 BlockingCollection 旨在包装并发集合并提供允许多个消费者阻塞的机制;等待生产者。这种用法似乎与您的要求相反。

我发现这个article about the BlockingCollection class 很有帮助。

【讨论】:

  • 很遗憾,我真的做不到。有许多单独的队列在 IO 完成时被非常稀疏地填满。可能有数千个。然后仅在另一个 IO 事件发生时才消耗这些事件,该事件在 IO 完成回调中运行,因此不能阻塞。本质上,存在不频繁的生产者和消费者,它们都在 IO 完成上运行。消费者需要知道集合是否在给定的超时时间内添加了项目,而不会将自己置于阻塞调用中。
  • 消费者如何映射到队列?每个队列一个消费者?消费者是否会遍历队列寻找产品?
  • 消费者在 IO 回调上异步到达,需要从集合中拉出东西,或者如果在此期间集合没有填充数据,则需要等待一段时间才能放弃。没有迭代,基本上就是一个队列
  • 对不起;我试图询问消费者是否遍历数千个队列,而不是队列本身。感谢您提供进一步的见解。
【解决方案2】:

所以看起来没有内置选项,我出去尝试尽我所能做我想要的实验。事实证明,要使这项工作与旧异步模式的其他用户大致相同,还有很多工作要做。

public class AsyncQueue<T>
{
    private readonly ConcurrentQueue<T> queue;
    private readonly ConcurrentQueue<DequeueAsyncResult> dequeueQueue; 

    private class DequeueAsyncResult : IAsyncResult
    {
        public bool IsCompleted { get; set; }
        public WaitHandle AsyncWaitHandle { get; set; }
        public object AsyncState { get; set; }
        public bool CompletedSynchronously { get; set; }
        public T Result { get; set; }

        public AsyncCallback Callback { get; set; }
    }

    public AsyncQueue()
    {
        dequeueQueue = new ConcurrentQueue<DequeueAsyncResult>();
        queue = new ConcurrentQueue<T>();
    }

    public void Enqueue(T item)
    {
        DequeueAsyncResult asyncResult;
        while  (dequeueQueue.TryDequeue(out asyncResult))
        {
            if (!asyncResult.IsCompleted)
            {
                asyncResult.IsCompleted = true;
                asyncResult.Result = item;

                ThreadPool.QueueUserWorkItem(state =>
                {
                    if (asyncResult.Callback != null)
                    {
                        asyncResult.Callback(asyncResult);
                    }
                    else
                    {
                        ((EventWaitHandle) asyncResult.AsyncWaitHandle).Set();
                    }
                });
                return;
            }
        }
        queue.Enqueue(item);
    }

    public IAsyncResult BeginDequeue(int timeout, AsyncCallback callback, object state)
    {
        T result;
        if (queue.TryDequeue(out result))
        {
            var dequeueAsyncResult = new DequeueAsyncResult
            {
                IsCompleted = true, 
                AsyncWaitHandle = new EventWaitHandle(true, EventResetMode.ManualReset), 
                AsyncState = state, 
                CompletedSynchronously = true, 
                Result = result
            };
            if (null != callback)
            {
                callback(dequeueAsyncResult);
            }
            return dequeueAsyncResult;
        }

        var pendingResult = new DequeueAsyncResult
        {
            AsyncState = state, 
            IsCompleted = false, 
            AsyncWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset), 
            CompletedSynchronously = false,
            Callback = callback
        };
        dequeueQueue.Enqueue(pendingResult);
        Timer t = null;
        t = new Timer(_ =>
        {
            if (!pendingResult.IsCompleted)
            {
                pendingResult.IsCompleted = true;
                if (null != callback)
                {
                    callback(pendingResult);
                }
                else
                {
                    ((EventWaitHandle)pendingResult.AsyncWaitHandle).Set();
                }
            }
            t.Dispose();
        }, new object(), timeout, Timeout.Infinite);

        return pendingResult;
    }

    public T EndDequeue(IAsyncResult result)
    {
        var dequeueResult = (DequeueAsyncResult) result;
        return dequeueResult.Result;
    }
}

我不太确定IsComplete 属性的同步,我也不太关心dequeueQueue 如何仅在随后的Enqueue 调用中得到清理。我也不确定何时是发出等待句柄信号的正确时间,但这是迄今为止我得到的最佳解决方案。

请不要以任何方式考虑此生产质量代码。我只是想展示我如何在不等待锁的情况下保持所有线程旋转的一般要点。我确信这充满了各种边缘情况和错误,但它满足了要求,我想回馈遇到这个问题的人。

【讨论】:

  • 我并不完全了解您的线程模型。无论回调是否完成或超时,您都可以在 EndDequeue 中访问结果。如果您在 IsCompleted 属性上循环直到得到答案,您仍然会阻塞线程。
  • 它不会阻塞,因为它在计时器上运行,它不会启动线程,而只会在池中排队任务。 Enddequeue 也会在超时时调用,在这些情况下结果将是默认值。
  • 我的意思是谁在调用 EndDequeue? Begin/End 模式确保您可以产生工作,但在某些时候您需要调用相应的 End 方法。您当前所做的是注册回调并在 Enqueue 方法期间调用它们,如果同时某些数据确实到达,则通过计时器稍等片刻。你可以在没有计时器的情况下做到这一点,只需有一个观察者队列,你在入队时存储他们的入队时间。当数据到达时,您将所有太旧的观察者出列并跳过它们并调用尚未过期的回调。
  • @AloisKraus 也许我误解了你,但 EndDequeue 应该从回调中调用,或者在 IAsyncResult 中暴露的等待句柄发出信号之后。无论数据是否到达,我都希望在超时时调用回调。使用这样的观察者队列并不能确保在超时情况下调用回调,因此只有实际接收到数据的调用才会调用它们的回调。
  • 如果许多观察者同时超时,您执行读取策略的方式仍然可能导致线程池耗尽(请参阅stackoverflow.com/questions/31581/…)。您可以使用一个全局计时器,它会不时检查未决的 IAsyncResults,在我看来这会更安全。
【解决方案3】:

我很确定 BlockingCollection&lt;T&gt; 不能这样做,你必须自己动手。我想出了这个:

class NotifyingCollection<T>
{
    private ConcurrentQueue<Action<T>> _subscribers = new ConcurrentQueue<Action<T>>();
    private ConcurrentQueue<T> _overflow = new ConcurrentQueue<T>();

    private object _lock = new object();

    public void Add(T item)
    {
        _overflow.Enqueue(item);
        Dispatch();
    }

    private void Dispatch()
    {
        // this lock is needed since we need to atomically dequeue from both queues...
        lock (_lock)
        {
            while (_overflow.Count > 0 && _subscribers.Count > 0)
            {
                Action<T> callback;
                T item;

                var r1 = _overflow.TryDequeue(out item);
                var r2 = _subscribers.TryDequeue(out callback);

                Debug.Assert(r1 && r2);
                callback(item);
                // or, optionally so that the caller thread's doesn't take too long ...
                Task.Factory.StartNew(() => callback(item));
                // but you'll have to consider how exceptions will be handled.
            }
        }
    }

    public void TakeAsync(Action<T> callback)
    {
        _subscribers.Enqueue(callback);
        Dispatch();
    }
}

我使用了调用TakeAsync()Add() 的线程作为回调线程。当您调用Add()TakeAsync() 时,它将尝试将所有排队的项目分派到排队的回调中。这样就不会创建只是坐在那里睡觉,等待发出信号的线程。

那个锁有点难看,但是你可以在不加锁的情况下在多个线程上排队和订阅。我想不出一种方法来做相当于只有在另一个队列上有可用的东西时才出队而不使用那个锁。

注意:我只对此进行了最低限度的测试,有几个线程。

【讨论】:

  • 嗯,是的。唯一的问题是您将使用您的方法创建一个锁定的线程,考虑到非常高的负载,这将是危险的。 TryTake 调用阻塞。在您有数千个这样的调用的情况下,您会用完任务线程池中的线程并锁定您的应用程序。
  • 啊!你完全不想要阻塞线程;我以为你只是想确保调用线程不会被阻塞。我现在明白了。
猜你喜欢
  • 1970-01-01
  • 2018-04-06
  • 1970-01-01
  • 2016-07-14
  • 1970-01-01
  • 2022-09-29
  • 2020-10-28
  • 2016-02-15
  • 2018-12-31
相关资源
最近更新 更多