BlockingCollection(T).GetConsumingEnumerable()如何抛出OperationCanceledException?

时间:2014-04-09 15:44:55

标签: c# task-parallel-library

我正在使用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#

3 个答案:

答案 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,如果在调用OperationCanceledExceptionBlockingCollection为空,则底层框架代码会抛出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框架函数内发生的异常进行破坏。