我正在使用(坦率的)BlockingCollection<>
类型来处理多线程,高性能的应用程序。
通过集合有很多吞吐量,在微观层面上它具有很高的性能。但是,对于每个“批次”,它将始终通过标记取消令牌来结束。这导致在任何等待Take
调用时抛出异常。这没关系,但是我会确定一个返回值或输出参数来表示它,因为a)异常有明显的开销,而b)在调试时,我不想手动关闭那个特定的break-on-exception异常。
实现看起来很激烈,理论上我认为我可以反汇编并重新创建我自己的版本而不使用异常,但是可能有一种不太复杂的方式?
我可以在集合中添加null
(或者如果不是占位符)对象来表示进程应该结束,但是还需要有一种方法可以很好地中止,即唤醒等待线程并告诉他们不知怎的,事情已经发生了。
那么 - 替代收藏类型?重新创建自己的?有人滥用这个吗?
(某些上下文:我选择了BlockingCollection<>
,因为它比Queue
周围的手动锁定更有优势。最好的我可以告诉使用线程原语是极好的,在我的情况下,一些毫秒在这里和那里,最佳核心是至关重要的。)
编辑:我刚刚为此开了一笔赏金。 我不相信Anastasiosyal的答案涵盖了我在评论中提出的问题。我知道这是一个棘手的问题。有人能帮忙吗?
答案 0 :(得分:9)
正如我猜你已经完成了自己,查看BlockingCollection的反映来源,不幸的是,当CancellationToken被传递到BlockingCollection并取消时,你会得到OperationCancelledException,如下图所示(带有图像之后的几个解决方法)
GetConsumingEnumerable
在BlockingCollection上调用TryTakeWithNoTimeValidation
,这反过来会引发此异常。
解决方法#1
假设您对生产者和消费者有更多控制权,而不是将取消令牌传递给BlockingCollection(这将引发此异常),您可以将取消令牌传递给您的生产者和消费者
如果您的生产者没有生产并且您的消费者没有消费,那么您已经有效地取消了操作而没有引发此异常并且在您的BlockingCollection中传递CancellationToken.None。
特殊情况当BlockingCollection处于BoundedCapacity或空时取消
生产者被阻止:当达到BlockingCollection上的BoundedCapacity时,生产者线程将被阻止。因此,当尝试取消并且BlockingCollection处于BoundedCapacity时(这意味着您的消费者未被阻止但生产者因为无法向队列中添加任何其他项而被阻止),那么您将需要允许使用其他项目(一个对于每个生成器线程)将解除生成器的阻塞(因为它们在添加到blockingCollection时被阻止),反过来允许你的取消逻辑在生产者端启动。
阻止消费者:当您的消费者因队列为空而被阻止时,您可以在阻止集合中插入一个空的工作单元(每个消费者线程一个),以便取消阻止消费者线程并允许您的取消逻辑在消费者方面踢。
如果队列中有项目且未达到BoundedCapacity或Empty等限制,则不应阻止生产者和消费者线程。
解决方法#2
使用取消工作单位。
当您的应用程序需要取消时,那么您的生产者(可能只有1个生产者就足够了,而其他人只是取消生产)将产生一个取消工作单元(可能是null,因为您还提到或某些实现标记接口的类)。当消费者使用这个工作单元并检测到它实际上是一个取消工作单元时,他们的取消逻辑就会起作用。要生产的取消工作单元的数量需要等于消费者线程的数量。
同样,当我们接近BoundedCapacity时需要谨慎,因为这可能是一些生产者被阻止的迹象。根据生产者/消费者的数量,在所有生产者(但是1)关闭之前,消费者可以消费。这确保了周围没有挥之不去的生产者。当只有一个生产者离开时,您的最后一个消费者可以关闭,生产者可以停止生产取消工作单位。
答案 1 :(得分:1)
我之前做过的BlockingQueue怎么样?
没有任何例外,它应该没问题。当前队列只是关闭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
}
}
答案 2 :(得分:1)
您可以通过在最后一项上设置一个标志来向批处理结束发出信号(向其添加一个IsLastItem bool属性或将其包装)。或者你可以发送一个null作为最后一项(不确定null是否正确地通过了阻塞集合)。
如果您可以删除对“批处理”概念的需要,您可以创建一个额外的线程来连续Take()并从您的blockingcollection处理新数据,而不执行任何其他操作。
答案 3 :(得分:1)
基伦,
从我的检查中,我个人不知道ProducerConsumer模式的任何线程安全类型,它完全符合您的要求。我并不认为这是一个有竞争力的解决方案,但建议您使用BlockingCollection<T>
来装饰extension method
,这样您就可以自由地提供任何内置或自定义类型,而不是默认CancellationToken
。< / p>
第1阶段:
以下是使用under 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);
}
现在,您可以为您感兴趣的任何/所有方法提供扩展。
第2阶段:
现在,您可以参考TryAddWithNoTimeValidation
的实施,而不是默认。
我可以为您提供TryAddWithNoTimeValidation
的替代版本,可以安全地继续播放而不会抛出OperationCancellation
例外。