简体   繁体   English

使用 BlockingCollection<T> :OperationCanceledException,有没有更好的办法?

[英]Using BlockingCollection<T>: OperationCanceledException, is there a better way?

I'm making use of the (frankly great) BlockingCollection<T> type for a heavily multithreaded, high-performance app.我正在将(坦率地说很棒) BlockingCollection<T>类型用于高度多线程的高性能应用程序。

There's a lot of throughput through the collection and on the micro-level it's highly performant.整个集合有很多吞吐量,并且在微观层面上它是高性能的。 However, for each 'batch' it will always be ended by flagging the cancellation token.但是,对于每个“批次”,它总是通过标记取消标记来结束。 This results in an exception being thrown on any waiting Take call.这会导致在任何等待的Take调用上抛出异常。 That's fine, but I would have settled for a return value or output parameter to signal it, because a) exceptions have an obvious overhead and b) when debugging, I don't want to manually turn off break-on-exception for that specific exception.这很好,但我会选择一个返回值或输出参数来表示它,因为 a) 异常有明显的开销 b) 在调试时,我不想手动关闭那个特定的异常中断例外。

The implementation seems intense, and in theory I suppose I could disassemble and recreate my own version that didn't use exceptions, but perhaps there's a less complex way?实现似乎很激烈,理论上我想我可以反汇编并重新创建我自己的不使用异常的版本,但也许有更简单的方法?

I could add a null (or if not, a placeholder) object to the collection to signify the process should end, however there also needs to be a means to abort nicely, ie wake up waiting threads and tell them somehow that something's gone on.我可以向集合中添加一个null (或者如果不是,一个占位符)对象来表示进程应该结束,但是也需要有一种方法可以很好地中止,即唤醒等待线程并以某种方式告诉他们某些事情已经发生。

So - alternative collection types?那么 - 替代集合类型? Recreate my own?重新创建我自己的? Some way to abuse this one?某种方式来滥用这个?

(Some context: I went with BlockingCollection<T> because it has an advantage over manual locking around a Queue . As best I can tell the use of threading primitives is superb and in my case, a few milliseconds here-and-there and optimal core is use crucial.) (一些上下文:我选择了BlockingCollection<T>因为它比手动锁定Queue有优势。尽我所能说线程原语的使用是极好的,在我的情况下,这里和那里几毫秒并且是最佳的核心是使用至关重要。)

Edit: I've just opened a bounty for this one.编辑:我刚刚为这个开了一个赏金。 I don't believe Anastasiosyal's answer covers the query I raise in my comment of it.我不相信Anastasiosyal 的回答涵盖了我在评论中提出的问题。 I know this is a tough problem.我知道这是一个棘手的问题。 Is anyone able to assist?有人可以提供帮助吗?

As I guess you have already done yourself, looking into the reflected source of BlockingCollection it looks unfortunately that when a CancellationToken is passed into the BlockingCollection and it cancels then you will get the OperationCancelledException as can be seen in the image below (with a couple of workarounds after the image) 我猜你已经完成了自己,看看BlockingCollection的反映来源,不幸的是,当一个CancellationToken被传递到BlockingCollection并取消时,你会得到OperationCancelledException,如下图所示(有几个图像后的变通方法)

GetConsumingEnumerable invokes TryTakeWithNoTimeValidation on the BlockingCollection which in turn raises this exception. GetConsumingEnumerable在BlockingCollection上调用TryTakeWithNoTimeValidation ,后者又引发此异常。

在此输入图像描述

Workaround #1 解决方法#1

One potential strategy would be, assuming you have more control over your producers and your consumers, rather than pass the cancellation token into the BlockingCollection, (which will raise this exception) you pass the cancellation token into your producers and into your consumers. 一种可能的策略是,假设您对生产者和消费者有更多控制权,而不是将取消令牌传递给BlockingCollection(这将引发此异常),您将取消令牌传递给您的生产者和消费者。

If your producers aren't producing and your consumers aren't consuming, then you have effectively cancelled the operation without raising this exception and by passing CancellationToken.None in your BlockingCollection. 如果您的生产者没有生产并且您的消费者没有消费,那么您已经有效地取消了操作,而没有引发此异常并且在您的BlockingCollection中传递CancellationToken.None。

Special cases Cancelling when the BlockingCollection is at BoundedCapacity or Empty 特殊情况当BlockingCollection处于BoundedCapacity或Empty时取消

Producers blocked : The producer threads will be blocked when BoundedCapacity on the BlockingCollection is reached. 生产者被阻止 :当达到BlockingCollection上的BoundedCapacity时,生产者线程将被阻止。 Hence, when attempting to cancel and the BlockingCollection is at BoundedCapacity (which means that your consumers are not blocked but producers are blocked because they cannot add any additional items to the queue) then you will need to allow for additional items to be consumed (one for each producer thread) that will unblock the producers (because they are blocked on adding to the blockingCollection) and in turn allow for your cancellation logic to kick in on the producer side. 因此,当尝试取消并且BlockingCollection处于BoundedCapacity时(这意味着您的消费者未被阻止但生产者因为无法向队列中添加任何其他项而被阻止),那么您将需要允许使用其他项目(一个对于每个生成器线程)将解除生成器的阻塞(因为它们在添加到blockingCollection时被阻止),反过来允许你的取消逻辑在生产者端启动。

Consumers blocked : When your consumers are blocked because the queue is empty, then you could insert an empty unit of work (one for each consumer thread) in the Blocking collection so as to unblock the consumer threads and allow for your cancellation logic to kick in the consumer side. 消费者被阻止 :当您的消费者因队列为空而被阻止时,您可以在阻止集合中插入一个空的工作单元(每个消费者线程一个),以便取消阻止消费者线程并允许您的取消逻辑启动消费者方面。

When there are items in the queue and no limit such as BoundedCapacity or Empty has been reached then the producers and consumer threads should not be blocked. 如果队列中有项目且未达到BoundedCapacity或Empty等限制,则不应阻止生产者和消费者线程。

Workaround #2 解决方法#2

Using a cancellation unit of work. 使用取消工作单位。

When your application needs to cancel, then your producers (maybe just 1 producer will suffice while the others just cancel producing) will produce a cancellation unit of work (could be null as you also mention or some class that implements a marker interface). 当您的应用程序需要取消时,那么您的生产者(可能只有1个生产者就足够而其他人只是取消生产)将产生一个取消工作单元(可能是null,因为您还提到或某些实现标记接口的类)。 When the consumers consume this unit of work and detect that it is in fact a cancellation unit of work, their cancellation logic kicks in. The number of cancellation units of work to be produced needs to equal the number of consumer threads. 当消费者使用这个工作单元并检测到它实际上是一个取消工作单元时,他们的取消逻辑就会起作用。要生产的取消工作单元的数量需要等于消费者线程的数量。

Again, caution is needed when we are close to BoundedCapacity, as it could be a sign that some of the producers are blocked. 当我们接近BoundedCapacity时,需要谨慎,因为这可能是一些生产者被阻止的迹象。 Depending on the number of producers/consumers you could have a consumer consuming until all producers (but 1) have shut down. 根据生产者/消费者的数量,在所有生产者(但是1)关闭之前,消费者可以消费。 This ensures that there are no lingering producers around. 这确保了周围没有挥之不去的生产者。 When there is only 1 producer left, your last consumer can shut down and the producer can stop producing cancellation units of work. 当只有一个生产者离开时,您的最后一个消费者可以关闭,生产者可以停止生产取消工作单位。

How about the BlockingQueue I did a while ago? 我之前做过的BlockingQueue怎么样?

http://apichange.codeplex.com/SourceControl/changeset/view/76c98b8c7311#ApiChange.Api%2fsrc%2fInfrastructure%2fBlockingQueue.cs http://apichange.codeplex.com/SourceControl/changeset/view/76c98b8c7311#ApiChange.Api%2fsrc%2fInfrastructure%2fBlockingQueue.cs

It should do fine without any exceptions. 没有任何例外,它应该没问题。 The current queue does simply close the event on dispose which might not be what you want. 当前队列只是关闭dispose上的事件,这可能不是你想要的。 You might want do enque a null and wait until all items were processed. 您可能希望将null设置为null并等待所有项目都被处理。 Apart from this it should suit your needs. 除此之外,它应该适合您的需求。

using System.Collections.Generic;
using System.Collections;
using System.Threading;
using System;

namespace ApiChange.Infrastructure
{

    /// <summary>
    /// A blocking queue which supports end markers to signal that no more work is left by inserting
    /// a null reference. This constrains the queue to reference types only. 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class BlockingQueue<T> : IEnumerable<T>, IEnumerable, IDisposable where T : class
    {
        /// <summary>
        /// The queue used to store the elements
        /// </summary>
        private Queue<T> myQueue = new Queue<T>();
        bool myAllItemsProcessed = false;
        ManualResetEvent myEmptyEvent = new ManualResetEvent(false);

        /// <summary>
        /// Deques an element from the queue and returns it.
        /// If the queue is empty the thread will block. If the queue is stopped it will immedieately
        /// return with null.
        /// </summary>
        /// <returns>An object of type T</returns>      
        public T Dequeue()
        {
            if (myAllItemsProcessed)
                return null;

            lock (myQueue)
            {
                while (myQueue.Count == 0) 
                {
                    if(!Monitor.Wait(myQueue, 45))
                    {
                        // dispatch any work which is not done yet
                        if( myQueue.Count > 0 )
                            continue;
                    }

                    // finito 
                    if (myAllItemsProcessed)
                    {
                        return null;
                    }
                }

                T result = myQueue.Dequeue();
                if (result == null)
                {
                    myAllItemsProcessed = true;
                    myEmptyEvent.Set();
                }
                return result;
            }
        }

        /// <summary>
        /// Releases the waiters by enqueuing a null reference which causes all waiters to be released. 
        /// The will then get a null reference as queued element to signal that they should terminate.
        /// </summary>
        public void ReleaseWaiters()
        {
            Enqueue(null);
        }

        /// <summary>
        /// Waits the until empty. This does not mean that all items are already process. Only that
        /// the queue contains no more pending work. 
        /// </summary>
        public void WaitUntilEmpty()
        {
            myEmptyEvent.WaitOne();
        }

        /// <summary>
        /// Adds an element of type T to the queue. 
        /// The consumer thread is notified (if waiting)
        /// </summary>
        /// <param name="data_in">An object of type T</param>      
        public void Enqueue(T data_in)
        {
            lock (myQueue)
            {
                myQueue.Enqueue(data_in);
                Monitor.PulseAll(myQueue);
            }
        }

        /// <summary>
        /// Returns an IEnumerator of Type T for this queue
        /// </summary>
        /// <returns></returns>    
        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            while (true)
            {
                T item = Dequeue();
                if (item == null)
                    break;
                else
                    yield return item;
            }
        }

        /// <summary>
        /// Returns a untyped IEnumerator for this queue
        /// </summary>
        /// <returns></returns>     
        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable<T>)this).GetEnumerator();
        }


        #region IDisposable Members

        /// <summary>
        /// Closes the EmptyEvent WaitHandle.
        /// </summary>
        public void Dispose()
        {
            myEmptyEvent.Close();
        }

        #endregion
    }
}

You cound signal the end of a batch by setting a flag on the last item (add a IsLastItem bool property to it or wrap it). 您可以通过在最后一项上设置一个标志来向批处理结束发出信号(向其添加一个IsLastItem bool属性或将其包装)。 Or you might send a null as last item (not sure if a null goes through the blockingcollection correctly though). 或者你可以发送一个null作为最后一项(不确定null是否正确地通过了阻塞集合)。

If you can remove the need for the 'batch' concept you can create an extra thread to continously Take() and Process new Data from your blockingcollection and do nothing else. 如果您可以删除对“批处理”概念的需要,您可以创建一个额外的线程来连续Take()并从您的blockingcollection处理新数据,而不执行任何其他操作。

Kieren, 基伦,

From my inspection, I personally don't know any thread safe type for ProducerConsumer pattern which does exactly what you wanted. 根据我的检查,我个人不知道ProducerConsumer模式的任何线程安全类型,它完全符合您的要求。 I don't claim this as competitive solution but propose you decorate BlockingCollection<T> with few extension method which will give you the freedom to supply any built-in or custom types instead of default CancellationToken . 我并不认为这是竞争解决方案,但建议您使用少量extension method装饰BlockingCollection<T> ,这将使您可以自由地提供任何内置或自定义类型而不是默认的CancellationToken

Stage 1: 阶段1:

Following are the list of default method which use underling TryAddWithNoTimeValidation method to add to queue. 以下是使用基础TryAddWithNoTimeValidation方法添加到队列的默认方法列表。

public void Add(T item){
      this.TryAddWithNoTimeValidation(item, -1, new CancellationToken());
}

public void Add(T item, CancellationToken cancellationToken){
      this.TryAddWithNoTimeValidation(item, -1, cancellationToken);
    }

public bool TryAdd(T item){
      return this.TryAddWithNoTimeValidation(item, 0, new CancellationToken());
    }

public bool TryAdd(T item, TimeSpan timeout){
      BlockingCollection<T>.ValidateTimeout(timeout);
      return this.TryAddWithNoTimeValidation(item, (int) timeout.TotalMilliseconds, new CancellationToken());
    }

public bool TryAdd(T item, int millisecondsTimeout){
      BlockingCollection<T>.ValidateMillisecondsTimeout(millisecondsTimeout);
      return this.TryAddWithNoTimeValidation(item, millisecondsTimeout, new           CancellationToken());
}

public bool TryAdd(T item, int millisecondsTimeout, CancellationToken cancellationToken){
 BlockingCollection<T>.ValidateMillisecondsTimeout(millisecondsTimeout);
 return this.TryAddWithNoTimeValidation(item, millisecondsTimeout, cancellationToken);
}

Now you can provide extension for any/all of method which you are interested. 现在,您可以为您感兴趣的任何/所有方法提供扩展。

Stage 2: 第二阶段:

You now refer your implementation of TryAddWithNoTimeValidation instead of default. 您现在引用TryAddWithNoTimeValidation的实现而不是默认值。

I can give you an alternate version of TryAddWithNoTimeValidation which safely continue without throwing OperationCancellation exception. 我可以给你一个替代版本的TryAddWithNoTimeValidation ,它可以安全地继续而不会抛出OperationCancellation异常。

My suggestion is to implement this functionality by encapsulating an asynchronous queue, like the BufferBlock<T> class from the TPL Dataflow library.我的建议是通过封装一个异步队列来实现这个功能,比如来自TPL Dataflow库的BufferBlock<T>类。 This class is a thread-safe container intended for producer-consumer scenarios, and supports backpressure ( BoundedCapacity ) just like the BlockingCollection<T> class.此类是用于生产者-消费者场景的线程安全容器,并像BlockingCollection<T>类一样支持背压 ( BoundedCapacity )。 Being asynchronous means that the corresponding Add / Take methods ( SendAsync / ReceiveAsync ) return tasks.异步意味着相应的Add / Take方法( SendAsync / ReceiveAsync )返回任务。 These tasks store the event of a cancellation as an internal state, that can be queried with the IsCanceled property, so throwing exceptions internally can be avoided.这些任务将取消事件存储为内部状态,可以使用IsCanceled属性进行查询,因此可以避免在内部抛出异常。 Propagating those exceptions can also be avoided by awaiting the tasks using a specialized awaiter that suppresses all exceptions.通过使用抑制所有异常的专用等待器等待任务,也可以避免传播这些异常。 Here is an implementation:这是一个实现:

/// <summary>
/// A thread-safe collection that provides blocking and bounding capabilities.
/// The cancellation is propagated as a false result, and not as an exception.
/// </summary>
public class CancellationFriendlyBlockingCollection<T>
{
    private readonly BufferBlock<T> _bufferBlock;

    public CancellationFriendlyBlockingCollection()
    {
        _bufferBlock = new BufferBlock<T>();
    }

    public CancellationFriendlyBlockingCollection(int boundedCapacity)
    {
        _bufferBlock = new BufferBlock<T>(new() { BoundedCapacity = boundedCapacity });
    }

    public bool TryAdd(T item, CancellationToken cancellationToken = default)
    {
        if (_bufferBlock.Post(item)) return true;
        Task<bool> task = _bufferBlock.SendAsync(item, cancellationToken);
        SuppressException.Await(task).GetResult();
        Debug.Assert(task.IsCompleted);
        if (task.IsCanceled) return false;
        return task.GetAwaiter().GetResult();
    }

    public bool TryTake(out T item, CancellationToken cancellationToken = default)
    {
        if (_bufferBlock.TryReceive(out item)) return true;
        Task<T> task = _bufferBlock.ReceiveAsync(cancellationToken);
        SuppressException.Await(task).GetResult();
        Debug.Assert(task.IsCompleted);
        if (task.IsCanceled) return false;
        item = task.GetAwaiter().GetResult();
        return true;
    }

    public IEnumerable<T> GetConsumingEnumerable(
        CancellationToken cancellationToken = default)
    {
        while (TryTake(out var item, cancellationToken))
            yield return item;
    }

    public void CompleteAdding() => _bufferBlock.Complete();
    public Task Completion => _bufferBlock.Completion;

    private struct SuppressException : ICriticalNotifyCompletion
    {
        private Task _task;
        public static SuppressException Await(Task task)
            => new SuppressException { _task = task };
        public SuppressException GetAwaiter() => this;
        public bool IsCompleted => _task.IsCompleted;
        public void OnCompleted(Action action)
            => _task.GetAwaiter().OnCompleted(action);
        public void UnsafeOnCompleted(Action action)
            => _task.GetAwaiter().UnsafeOnCompleted(action);
        public void GetResult() { }
    }
}

The internal SuppressException struct is copy-pasted from this GitHub issue.内部SuppressException结构是复制粘贴GitHub的问题。 It is authored by a top Microsoft engineer (Stephen Toub).它由顶级 Microsoft 工程师 (Stephen Toub) 编写。

The CancellationFriendlyBlockingCollection.TryTake method can be invoked with a canceled CancellationToken in a loop with a frequency of about 1,500,000 times per second in my PC (on a single thread). CancellationFriendlyBlockingCollection.TryTake方法可以在我的 PC 中(在单个线程上)以每秒约 1,500,000 次的频率在循环中使用取消的CancellationToken调用。 For comparison the frequency of the BlockingCollection<T>.Take under the same conditions is about 10,000 times per second.为了比较, BlockingCollection<T>.Take在相同条件下的频率约为每秒 10,000 次。

You might be tempted to replace the BufferBlock<T> with a more modern asynchronous queue like the Channel<T> .您可能想用更现代的异步队列(如Channel<T>替换BufferBlock<T> Channel<T> In that case please make sure to read this question first, in order to be aware about a leaky behavior of this class, under specific conditions.在这种情况下,请务必先阅读此问题,以便了解此类在特定条件下的泄漏行为。

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

相关问题 BlockingCollection(T).GetConsumingEnumerable()如何抛出OperationCanceledException? - How can BlockingCollection(T).GetConsumingEnumerable() throw OperationCanceledException? 在现有的IProducerConsumerCollection(T)上使用BlockingCollection(T) - Using BlockingCollection(T) on existing IProducerConsumerCollection(T) 有没有一种方法可以简单地返回BlockingCollection - Is there a way to simply return a BlockingCollection 将 OperationCanceledException 与 CancellationToken 相关联的正确方法 - Proper way to correlate an OperationCanceledException to a CancellationToken 什么是更好,更清洁的方式使用List <T> - What is a better, cleaner way of using List<T> 作业的BlockingCollection或Queue <T>? - BlockingCollection or Queue<T> for jobs? 在BlockingCollection <T>上调用Dispose - Calling Dispose on an BlockingCollection<T> 使用BlockingCollection时出现死锁 <T> 和TPL数据流一起 - Deadlocks when using BlockingCollection<T> and TPL dataflow together 使用多个BlockingCollection <T> 实现管道验证的缓冲区 - Using multiple BlockingCollection<T> buffers in implementing pipeline validation 使用BlockingCollection缩放连接<T>() - Scaling Connections with BlockingCollection<T>()
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM