.NET8中BlockingCollection<T>意外抛出InvalidOperationException问题咨询
BlockingCollection调用Take()时偶发InvalidOperationException的原因及解决方法
你在.NET 8环境下使用BlockingCollection<T>作为队列时,偶发以下异常:
System.InvalidOperationException: The collection argument is empty and has been marked as complete with regards to additions.
代码示例:
private static void TestMethod() { using BlockingCollection<DummyClass> queue = new BlockingCollection<DummyClass>(); var task = Task.Run(() => //Produce { for (int i = 0; i < 10000000; i++) { queue.Add(new DummyClass()); } queue.CompleteAdding(); }); int counter = 0; try { while (!queue.IsCompleted) //Consume { DummyClass item = queue.Take(); // <-- Sometimes exception here counter++; } Console.WriteLine($"counter={counter} "); } catch (Exception ex) { Console.WriteLine("Error:" + ex.ToString()); } }
你疑惑的点在于:IsCompleted的定义是“集合已标记添加完成且为空”,按逻辑未调用CompleteAdding()时Take()会阻塞;调用CompleteAdding()且队列为空时,!queue.IsCompleted应该返回false,不会进入循环调用Take()。
问题根源:竞态条件
问题出在!queue.IsCompleted的判断和queue.Take()的调用是两个独立的非原子操作,中间存在时序间隙:
- 当队列还剩最后一个元素,且生产者已调用
CompleteAdding()时,IsCompleted返回false(因为队列不为空),消费者进入循环体。 - 在消费者执行
Take()之前,若该元素被其他线程(多消费者场景)取走,此时队列变为空且已标记完成添加,Take()就会触发InvalidOperationException。 - 即使是单消费者场景,极端时序下也可能出现类似问题:比如消费者判断
!IsCompleted为true后,队列元素刚好被取空,同时CompleteAdding()完成标记,此时Take()会检测到集合已完成且为空,抛出异常。
IsCompleted只是当前时刻的状态快照,无法保证后续Take()操作时状态不变,两者之间没有原子性保障。
解决方法
推荐三种可靠的消费方式:
1. 使用GetConsumingEnumerable()(最简洁)
这是官方推荐的消费方式,自动处理阻塞、完成标记和空集合的情况,无需手动判断状态:
int counter = 0; try { foreach (var item in queue.GetConsumingEnumerable()) { counter++; } Console.WriteLine($"counter={counter} "); } catch (Exception ex) { Console.WriteLine("Error:" + ex.ToString()); }
2. 使用TryTake()替代Take()
通过TryTake()的返回值判断是否成功获取元素,避免异常:
int counter = 0; DummyClass item; while (queue.TryTake(out item, Timeout.Infinite)) { counter++; } Console.WriteLine($"counter={counter} ");
当集合已完成添加且为空时,TryTake()会返回false,自动退出循环。
3. 保留原结构但捕获特定异常
如果必须保留原循环结构,可以在循环内捕获InvalidOperationException并退出循环:
int counter = 0; try { while (true) { try { DummyClass item = queue.Take(); counter++; } catch (InvalidOperationException) { // 集合已完成添加且为空,退出循环 break; } } Console.WriteLine($"counter={counter} "); } catch (Exception ex) { Console.WriteLine("Error:" + ex.ToString()); }
内容的提问来源于stack exchange,提问作者Steve
相关产品推荐
相关产品推荐

