我正在使用BlockingCollection来实现任务调度程序,基本上是:
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);
}
(...)
}
我只看过一次并且无法重现这一点,但是在foreach的某个时刻(在TryTakeWithNoTimeValidation中)我得到了一个OperationCanceledException。我不明白因为我正在使用不采用CancellationToken的重载,而the documentation states它只能抛出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#
答案 0 :(得分:12)
这是一个老问题,但我会为将来发现它的人添加完整的答案。 Eugene提供的答案部分正确;当时你必须使用Visual Studio进行调试,配置为打破已处理的框架异常。
但是,您在OperationCanceledException
上突破的实际原因是BlockingCollection<T>.CompleteAdding()
的代码如下所示:
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
,如果在调用OperationCanceledException
时BlockingCollection
为空,则底层框架代码会抛出CompleteAdding()
。这样做是为了通知GetConsumingEnumerable()
方法退出。该异常由框架代码处理,如果您没有将调试器配置为拦截它,您就不会注意到它。
您无法复制它的原因是因为您在CompleteAdding()
方法中拨打了Dispose()
。因此,它在GC的一时兴起被召唤。
答案 1 :(得分:2)
我只能推测,但我认为您可能正在体验Stephen Toub在他的"Task.Wait and “Inlining”"博客文章和Jon Skeet here中描述的任务内联方案。
您对TaskScheduler.TryExecuteTaskInline
的实施情况如何?要防止意外的任务内联,请始终返回false
:
override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false;
}
答案 2 :(得分:2)
该异常偶尔发生在GetConsumingEnumerable枚举器的MoveNext()方法中,但它是一个处理过的异常,所以通常你不应该看到它。
也许您已将调试器配置为中断已处理的异常(在Visual Studio中,这些选项位于“调试/异常”菜单中),在这种情况下,调试器可能会对.NET框架函数内发生的异常进行破坏。