C# 使用BlockingCollection<>;:操作取消异常,有更好的方法吗?

C# 使用BlockingCollection<>;:操作取消异常,有更好的方法吗?,c#,multithreading,blockingcollection,C#,Multithreading,Blockingcollection,我正在使用(坦率地说是很棒的)BlockingCollection类型来实现一个高度多线程、高性能的应用程序 整个集合有很多吞吐量,在微观层面上,它的性能非常好。但是,对于每个“批次”,它总是通过标记取消令牌来结束。这将导致在任何等待的Take调用上引发异常。这很好,但我会选择返回值或输出参数来发出信号,因为a)异常有明显的开销,b)调试时,我不想手动关闭特定异常的中断异常 实现看起来很紧张,理论上我想我可以分解并重新创建我自己的版本,不使用异常,但也许有一种不那么复杂的方法 我可以在集合中添加

我正在使用(坦率地说是很棒的)
BlockingCollection
类型来实现一个高度多线程、高性能的应用程序

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

实现看起来很紧张,理论上我想我可以分解并重新创建我自己的版本,不使用异常,但也许有一种不那么复杂的方法

我可以在集合中添加一个
null
(或者如果没有,则添加一个占位符)对象,以表示进程应该结束,但是还需要有一种方法可以很好地中止,即唤醒等待的线程,并以某种方式告诉它们发生了什么事情

那么-其他收集类型?重建我自己的?有没有办法滥用这个

(一些上下文:我选择了
BlockingCollection
,因为它比
队列周围的手动锁定有优势。我可以说线程原语的使用非常出色,在我的例子中,几毫秒左右和最佳内核的使用至关重要。)


Edit:我刚刚为这一个打开了一个悬赏窗口。我不相信Anastasiosyal的回答涵盖了我在评论中提出的问题。我知道这是个棘手的问题。有人能提供帮助吗?

正如我猜您自己已经做过的那样,查看BlockingCollection的反射源,很不幸,当一个CancellationToken被传递到BlockingCollection并被取消时,您将得到OperationCancelled异常,如下图所示(图片后面有几个变通方法)

getconsumineGenumerable
对BlockingCollection调用
TryTakeWithNoTimeValidation
,这反过来会引发此异常

解决办法#1

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

如果您的生产者没有生产,而您的消费者没有消费,则您已在不引发此异常的情况下,通过在BlockingCollection中传递CancellationToken.None,有效地取消了操作

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

生产者被阻止:当达到BlockingCollection上的BoundedCapacity时,生产者线程将被阻止。因此,当尝试取消且BlockingCollection处于BoundedCapacity时(这意味着您的消费者未被阻止,但生产者被阻止,因为他们无法向队列添加任何其他项目)然后,您需要允许消费额外的项目(每个生产者线程一个),这将取消阻止生产者(因为它们在添加到blockingCollection时被阻止),并反过来允许取消逻辑在生产者端生效

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

当队列中有项目且未达到限制(如BoundedCapacity或Empty)时,不应阻止生产者和消费者线程

解决办法#2

使用一个工作单元

当您的应用程序需要取消时,您的生产者(可能只有一个生产者就足够了,而其他生产者只是取消生产)将生成一个取消工作单元(可以是空的,正如您所提到的,或者是实现标记接口的某个类)。当使用者使用此工作单元并检测到它实际上是一个取消工作单元时,其取消逻辑将生效。要生成的取消工作单元的数量需要等于使用者线程的数量


同样,当我们接近边界容量时,需要谨慎,因为这可能是一些生产商被封锁的迹象。根据生产商/消费者的数量,你可以让消费者消费,直到所有生产商(1除外)已关闭。这可确保周围没有延迟的制作人。当只剩下一个制作人时,您的最后一个消费者可以关闭,制作人可以停止生产取消工作单元。

我刚才做的封锁队列如何

它应该可以正常运行,没有任何异常。当前队列只是关闭dispose上的事件,这可能不是您想要的。您可能需要设置一个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
    }
}
使用System.Collections.Generic;
使用系统集合;
使用系统线程;
使用制度;
命名空间ApiChange.Infrastructure
{
/// 
///一种阻塞队列,它支持结束标记,以表明插入后不再有剩余工作
///空引用。这将队列限制为仅引用类型。
/// 
/// 
公共类阻止队列:IEnumerable,IEnumerable,IDisposable其中T:class
{
/// 
///用于存储元素的队列
/// 
专用队列myQueue=新队列();
布尔Myallitemsproced
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);
}