![](/img/trans.png)
[英]How to cancel GetConsumingEnumerable() on BlockingCollection
[英]How can BlockingCollection(T).GetConsumingEnumerable() throw OperationCanceledException?
我正在使用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的重載,並且文檔聲明它可能只拋出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#
這是一個老問題,但我會為將來發現它的人添加完整的答案。 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
,如果在調用CompleteAdding()
時BlockingCollection
為空,則底層框架代碼會拋出OperationCanceledException
。 它這樣做是為了通知GetConsumingEnumerable()
方法退出。 該異常由框架代碼處理,如果您沒有將調試器配置為攔截它,您就不會注意到它。
你無法復制它的原因是因為你在Dispose()
方法中調用了CompleteAdding()
。 因此,它在GC的一時興起被召喚。
我只能推測,但我認為你可能正在經歷由Stephen Toub在他的“Task.Wait和”內聯“博客文章和Jon Skeet 在這里描述的任務內聯方案。
你對TaskScheduler.TryExecuteTaskInline
的實現是什么樣的? 要防止意外的任務內聯,請始終返回false
:
override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return false;
}
該異常偶爾發生在GetConsumingEnumerable枚舉器的MoveNext()方法中,但它是一個處理過的異常,所以通常你不應該看到它。
也許您已將調試器配置為中斷已處理的異常(在Visual Studio中,這些選項位於Debug / Exceptions菜單中),在這種情況下,調試器可能會在.NET框架函數內發生異常時收支平衡。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.