C# BlockingCollection(T).GetConsumingEnumerable()如何抛出OperationCanceledException?
我使用BlockingCollection来实现任务调度器,基本上:C# BlockingCollection(T).GetConsumingEnumerable()如何抛出OperationCanceledException?,c#,task-parallel-library,C#,Task Parallel Library,我使用BlockingCollection来实现任务调度器,基本上: public class DedicatedThreadScheduler : TaskScheduler, IDisposable { readonly BlockingCollection<Task> m_taskQueue = new BlockingCollection<Task>(); readonly Thread m_thread; public Dedica
public class DedicatedThreadScheduler : TaskScheduler, IDisposable
{
readonly BlockingCollection<Task> m_taskQueue = new BlockingCollection<Task>();
readonly Thread m_thread;
public DedicatedThreadScheduler()
{
m_thread = new Thread(() =>
{
foreach (var task in m_taskQueue.GetConsumingEnumerable())
{
TryExecuteTask(task);
}
m_taskQueue.Dispose();
});
m_thread.Start();
}
public void Dispose()
{
m_taskQueue.CompleteAdding();
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return Thread.CurrentThread == m_thread && TryExecuteTask(task);
}
(...)
}
public类专用线程调度器:任务调度器,IDisposable
{
只读BlockingCollection m_taskQueue=新建BlockingCollection();
只读线程m_线程;
公共专用线程调度程序()
{
m_线程=新线程(()=>
{
foreach(m_taskQueue.GetConsumingEnumerable()中的var任务)
{
TryExecuteTask(任务);
}
m_taskQueue.Dispose();
});
m_thread.Start();
}
公共空间处置()
{
m_taskQueue.CompleteAdding();
}
受保护的覆盖bool TryExecuteTaskInline(任务任务,bool任务先前排队)
{
return Thread.CurrentThread==m_Thread&&TryExecuteTask(任务);
}
(...)
}
我只见过一次,无法重现,但在foreach(in)上的某个点上,我得到了一个OperationCanceledException。我不理解,因为我使用的重载不接受CancellationToken,并且它可能只抛出ObjectDisposedException。例外意味着什么?阻止收集已经完成了?队列中的任务已取消
更新:调用堆栈如下所示:
mscorlib.dll!System.Threading.SemaphoreSlim.WaitUntilCountOrTimeout(int millisecondsTimeout, uint startTime, System.Threading.CancellationToken cancellationToken) + 0x36 bytes
mscorlib.dll!System.Threading.SemaphoreSlim.Wait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken) + 0x178 bytes
System.dll!System.Collections.Concurrent.BlockingCollection<System.Threading.Tasks.Task>.TryTakeWithNoTimeValidation(out System.Threading.Tasks.Task item, int millisecondsTimeout, System.Threading.CancellationToken cancellationToken, System.Threading.CancellationTokenSource combinedTokenSource) Line 710 + 0x25 bytes C#
System.dll!System.Collections.Concurrent.BlockingCollection<System.Threading.Tasks.Task>.GetConsumingEnumerable(System.Threading.CancellationToken cancellationToken) Line 1677 + 0x18 bytes C#
public void CompleteAdding()
{
int num;
this.CheckDisposed();
if (this.IsAddingCompleted)
{
return;
}
SpinWait wait = new SpinWait();
Label_0017:
num = this.m_currentAdders;
if ((num & -2147483648) != 0)
{
wait.Reset();
while (this.m_currentAdders != -2147483648)
{
wait.SpinOnce();
}
}
else if (Interlocked.CompareExchange(ref this.m_currentAdders, num | -2147483648, num) == num)
{
wait.Reset();
while (this.m_currentAdders != -2147483648)
{
wait.SpinOnce();
}
if (this.Count == 0)
{
this.CancelWaitingConsumers();
}
this.CancelWaitingProducers();
}
else
{
wait.SpinOnce();
goto Label_0017;
}
}
mscorlib.dll!System.Threading.SemaphoreSlim.WaitUntillCountertimeout(int毫秒计时,uint开始时间,System.Threading.CancellationToken CancellationToken)+0x36字节
mscorlib.dll!System.Threading.SemaphoreSlim.Wait(int毫秒计时,System.Threading.CancellationToken CancellationToken)+0x178字节
System.dll!System.Collections.Concurrent.BlockingCollection.TryTakeWithNotTimeValidation(out System.Threading.Tasks.Task项,int毫秒计时,System.Threading.CancellationToken CancellationToken,System.Threading.CancellationTokenSource combinedTokenSource)行710+0x25字节C#
System.dll!System.Collections.Concurrent.BlockingCollection.GetConsumingEnumerable(System.Threading.CancellationToken CancellationToken)行1677+0x18字节C#
我只能猜测,但我认为您可能正在体验Stephen Toub在其博客文章和Jon Skeet所描述的任务内联场景
TaskScheduler.TryExecuteTaskInline
的实现是什么样子的?要防止意外的任务内联,请始终返回false
:
override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false;
}
该异常偶尔会发生在GetConsumingEnumerable枚举数的MoveNext()方法中,但它是一个已处理的异常,所以通常不应该看到它
也许您已将调试器配置为在处理异常时中断(在Visual Studio中,这些选项位于“调试/异常”菜单中),在这种情况下,调试器可能会在.NET framework函数中发生异常时实现收支平衡。这是一个老问题,但我将为将来找到它的任何人添加完整答案。尤金提供的答案部分是正确的;当时,您必须已使用配置为在处理框架异常时中断的Visual Studio进行调试 但是,您中断
操作取消异常的实际原因是BlockingCollection.completedadding()
的代码如下所示:
mscorlib.dll!System.Threading.SemaphoreSlim.WaitUntilCountOrTimeout(int millisecondsTimeout, uint startTime, System.Threading.CancellationToken cancellationToken) + 0x36 bytes
mscorlib.dll!System.Threading.SemaphoreSlim.Wait(int millisecondsTimeout, System.Threading.CancellationToken cancellationToken) + 0x178 bytes
System.dll!System.Collections.Concurrent.BlockingCollection<System.Threading.Tasks.Task>.TryTakeWithNoTimeValidation(out System.Threading.Tasks.Task item, int millisecondsTimeout, System.Threading.CancellationToken cancellationToken, System.Threading.CancellationTokenSource combinedTokenSource) Line 710 + 0x25 bytes C#
System.dll!System.Collections.Concurrent.BlockingCollection<System.Threading.Tasks.Task>.GetConsumingEnumerable(System.Threading.CancellationToken cancellationToken) Line 1677 + 0x18 bytes C#
public void CompleteAdding()
{
int num;
this.CheckDisposed();
if (this.IsAddingCompleted)
{
return;
}
SpinWait wait = new SpinWait();
Label_0017:
num = this.m_currentAdders;
if ((num & -2147483648) != 0)
{
wait.Reset();
while (this.m_currentAdders != -2147483648)
{
wait.SpinOnce();
}
}
else if (Interlocked.CompareExchange(ref this.m_currentAdders, num | -2147483648, num) == num)
{
wait.Reset();
while (this.m_currentAdders != -2147483648)
{
wait.SpinOnce();
}
if (this.Count == 0)
{
this.CancelWaitingConsumers();
}
this.CancelWaitingProducers();
}
else
{
wait.SpinOnce();
goto Label_0017;
}
}
请注意以下几行:
if (this.Count == 0)
{
this.CancelWaitingConsumers();
}
哪种方法调用此方法:
private void CancelWaitingConsumers()
{
this.m_ConsumersCancellationTokenSource.Cancel();
}
因此,即使您没有在代码中显式使用CancellationToken
,如果调用CompleteAdding()
时BlockingCollection
为空,则底层框架代码会抛出OperationCanceledException
。它这样做是为了向退出的getConsumineumerable()
方法发出信号。异常由框架代码处理,如果没有将调试器配置为拦截它,您不会注意到它
无法复制它的原因是您在Dispose()
方法中调用了CompleteAdding()
。因此,它是在GC的突发奇想下被调用的。您检查过堆栈跟踪了吗?异常可能发生在BlockingCollection
调用的方法中。是的,正如我在描述中添加的,它发生在TryTakeWIthNoTimeValidation中。就我所知,通过查看源代码,如果向它传递了CancellationToken,它应该只抛出OperationCanceledException,但我使用的重载不会…用调用堆栈更新问题。Dispose()方法和foreach循环之间没有互锁。通常可以正常工作,而不是因为某些原因过早调用Dispose()。@HansPassant这应该可以,因为阻塞集合的Dispose()必须在同一线程上的foreach之后调用。该类的Dispose()方法仅调用CompleteAdding(),以防止新任务排队。在问题中添加了TryExecuteTaskInline的实现。我将尝试在调用该方法时查看是否发生这种情况,感谢您的洞察力。这不是全部。我会把真正的原因写在一个单独的答案里。这是一个很好的答案!我想下一个明显的问题是我是否需要处理这个异常,如果需要,如何处理?这很可能发生在生产商多而消费者少的时候。不,你不需要处理它。它只是用作内部信号机制。只有将调试器配置为拦截它时,您才会意识到它。