This is an old question, but I'll add the full answer for anyone that finds it in the future. The answer provided by Eugene is partly correct; at the time you must have been debugging with Visual Studio configured to break on handled framework exceptions.
However, the actual reason you were breaking on an OperationCanceledException
is that the code for BlockingCollection<T>.CompleteAdding()
looks like this:
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;
}
}
Notice these particular lines:
if (this.Count == 0)
{
this.CancelWaitingConsumers();
}
which call this method:
private void CancelWaitingConsumers()
{
this.m_ConsumersCancellationTokenSource.Cancel();
}
So even though you weren't explicitly using a CancellationToken
in your code, the underlying framework code throws an OperationCanceledException
if the BlockingCollection
is empty when CompleteAdding()
is called. It does this to signal the GetConsumingEnumerable()
method to exit. The exception is handled by the framework code and you wouldn't have noticed it if you hadn't had your debugger configured to intercept it.
The reason you couldn't replicate it is because you placed your call to CompleteAdding()
in your Dispose()
method. Therefore, it was getting called at the whim of the GC.