繁体   English   中英

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

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

我正在将(坦率地说很棒) BlockingCollection<T>类型用于高度多线程的高性能应用程序。

整个集合有很多吞吐量,并且在微观层面上它是高性能的。 但是,对于每个“批次”,它总是通过标记取消标记来结束。 这会导致在任何等待的Take调用上抛出异常。 这很好,但我会选择一个返回值或输出参数来表示它,因为 a) 异常有明显的开销 b) 在调试时,我不想手动关闭那个特定的异常中断例外。

实现似乎很激烈,理论上我想我可以反汇编并重新创建我自己的不使用异常的版本,但也许有更简单的方法?

我可以向集合中添加一个null (或者如果不是,一个占位符)对象来表示进程应该结束,但是也需要有一种方法可以很好地中止,即唤醒等待线程并以某种方式告诉他们某些事情已经发生。

那么 - 替代集合类型? 重新创建我自己的? 某种方式来滥用这个?

(一些上下文:我选择了BlockingCollection<T>因为它比手动锁定Queue有优势。尽我所能说线程原语的使用是极好的,在我的情况下,这里和那里几毫秒并且是最佳的核心是使用至关重要。)

编辑:我刚刚为这个开了一个赏金。 我不相信Anastasiosyal 的回答涵盖了我在评论中提出的问题。 我知道这是一个棘手的问题。 有人可以提供帮助吗?

我猜你已经完成了自己,看看BlockingCollection的反映来源,不幸的是,当一个CancellationToken被传递到BlockingCollection并取消时,你会得到OperationCancelledException,如下图所示(有几个图像后的变通方法)

GetConsumingEnumerable在BlockingCollection上调用TryTakeWithNoTimeValidation ,后者又引发此异常。

在此输入图像描述

解决方法#1

一种可能的策略是,假设您对生产者和消费者有更多控制权,而不是将取消令牌传递给BlockingCollection(这将引发此异常),您将取消令牌传递给您的生产者和消费者。

如果您的生产者没有生产并且您的消费者没有消费,那么您已经有效地取消了操作,而没有引发此异常并且在您的BlockingCollection中传递CancellationToken.None。

特殊情况当BlockingCollection处于BoundedCapacity或Empty时取消

生产者被阻止 :当达到BlockingCollection上的BoundedCapacity时,生产者线程将被阻止。 因此,当尝试取消并且BlockingCollection处于BoundedCapacity时(这意味着您的消费者未被阻止但生产者因为无法向队列中添加任何其他项而被阻止),那么您将需要允许使用其他项目(一个对于每个生成器线程)将解除生成器的阻塞(因为它们在添加到blockingCollection时被阻止),反过来允许你的取消逻辑在生产者端启动。

消费者被阻止 :当您的消费者因队列为空而被阻止时,您可以在阻止集合中插入一个空的工作单元(每个消费者线程一个),以便取消阻止消费者线程并允许您的取消逻辑启动消费者方面。

如果队列中有项目且未达到BoundedCapacity或Empty等限制,则不应阻止生产者和消费者线程。

解决方法#2

使用取消工作单位。

当您的应用程序需要取消时,那么您的生产者(可能只有1个生产者就足够而其他人只是取消生产)将产生一个取消工作单元(可能是null,因为您还提到或某些实现标记接口的类)。 当消费者使用这个工作单元并检测到它实际上是一个取消工作单元时,他们的取消逻辑就会起作用。要生产的取消工作单元的数量需要等于消费者线程的数量。

当我们接近BoundedCapacity时,需要谨慎,因为这可能是一些生产者被阻止的迹象。 根据生产者/消费者的数量,在所有生产者(但是1)关闭之前,消费者可以消费。 这确保了周围没有挥之不去的生产者。 当只有一个生产者离开时,您的最后一个消费者可以关闭,生产者可以停止生产取消工作单位。

我之前做过的BlockingQueue怎么样?

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

没有任何例外,它应该没问题。 当前队列只是关闭dispose上的事件,这可能不是你想要的。 您可能希望将null设置为null并等待所有项目都被处理。 除此之外,它应该适合您的需求。

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

您可以通过在最后一项上设置一个标志来向批处理结束发出信号(向其添加一个IsLastItem bool属性或将其包装)。 或者你可以发送一个null作为最后一项(不确定null是否正确地通过了阻塞集合)。

如果您可以删除对“批处理”概念的需要,您可以创建一个额外的线程来连续Take()并从您的blockingcollection处理新数据,而不执行任何其他操作。

基伦,

根据我的检查,我个人不知道ProducerConsumer模式的任何线程安全类型,它完全符合您的要求。 我并不认为这是竞争解决方案,但建议您使用少量extension method装饰BlockingCollection<T> ,这将使您可以自由地提供任何内置或自定义类型而不是默认的CancellationToken

阶段1:

以下是使用基础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);
}

现在,您可以为您感兴趣的任何/所有方法提供扩展。

第二阶段:

您现在引用TryAddWithNoTimeValidation的实现而不是默认值。

我可以给你一个替代版本的TryAddWithNoTimeValidation ,它可以安全地继续而不会抛出OperationCancellation异常。

我的建议是通过封装一个异步队列来实现这个功能,比如来自TPL Dataflow库的BufferBlock<T>类。 此类是用于生产者-消费者场景的线程安全容器,并像BlockingCollection<T>类一样支持背压 ( BoundedCapacity )。 异步意味着相应的Add / Take方法( SendAsync / ReceiveAsync )返回任务。 这些任务将取消事件存储为内部状态,可以使用IsCanceled属性进行查询,因此可以避免在内部抛出异常。 通过使用抑制所有异常的专用等待器等待任务,也可以避免传播这些异常。 这是一个实现:

/// <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() { }
    }
}

内部SuppressException结构是复制粘贴GitHub的问题。 它由顶级 Microsoft 工程师 (Stephen Toub) 编写。

CancellationFriendlyBlockingCollection.TryTake方法可以在我的 PC 中(在单个线程上)以每秒约 1,500,000 次的频率在循环中使用取消的CancellationToken调用。 为了比较, BlockingCollection<T>.Take在相同条件下的频率约为每秒 10,000 次。

您可能想用更现代的异步队列(如Channel<T>替换BufferBlock<T> Channel<T> 在这种情况下,请务必先阅读此问题,以便了解此类在特定条件下的泄漏行为。

暂无
暂无

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

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