结论:裸用 Queue 的 Enqueue/Dequeue 在生产环境必须加防护,因其非线程安全、扩容时易抛 InvalidOperationException,多线程下应优先选用 ConcurrentQueue 或配合 lock + TryDequeue。

直接说结论:用 Queue<t></t> 做消息缓冲必须加防护,裸用 Enqueue/Dequeue 在生产环境大概率出问题,尤其在多线程、高吞吐或后台任务场景下。
为什么不能直接 new Queue() 初始化队列
写 new Queue() 看似能编译,但实际创建的是非泛型 Queue(来自 System.Collections),不是你想要的泛型 Queue<t></t>(来自 System.Collections.Generic)。两者完全无关,混用会导致:
- 装箱/拆箱开销,
int、bool等值类型性能直降 - 无类型检查,运行时才报
InvalidCastException - IDE 不提示方法、不支持 LINQ、
foreach中取值要手动as或Cast<t>()</t> - VS 提示 “'
Queue' is ambiguous” 就是这个原因——删掉using System.Collections;
正确写法只有一种:var q = new Queue<string>();</string> 或 C# 9+ 的 var q = new Queue<string>();</string>
消息缓冲中 Dequeue 抛 InvalidOperationException 的真实原因
很多人以为 Dequeue 只在空队列时抛异常,其实更常见的是内部数组扩容冲突:
- 当队列从容量 4 扩到 8 时,底层复制数组过程未加锁
- 此时另一个线程调用
Dequeue,可能读到半更新状态,直接抛InvalidOperationException: "Collection was modified" - 这种错误偶发、难复现,常出现在 ASP.NET Core 后台服务或 Unity 帧回调中
安全做法永远是:if (q.TryDequeue(out var msg)) { Process(msg); } —— 它不抛异常,返回 bool,且是原子操作(泛型 Queue<t></t> 的 TryDequeue 是 .NET 6+ 加入的)
多线程消息缓冲该选 Queue 还是 ConcurrentQueue
如果消息来自多个生产者(如多个 HTTP 请求、多个定时器、多个工作线程),Queue<t></t> 必须配 lock,但容易踩坑:
- 别在循环里反复
lock,应把一批Enqueue包进一个块 - 别锁
this或队列实例本身,用私有readonly object _lock = new(); -
Count > 0后再Dequeue仍是竞态:中间可能被其他线程取走
更优解是直接换 ConcurrentQueue<t></t>:
-
TryEnqueue和TryDequeue都是无锁、原子、不抛异常 - 没有扩容冲突问题,适合高频写入+消费场景
- 注意它不提供
Count的强一致性——并发下Count可能滞后,判空请用IsEmpty
Peek 和 Clear 在消息处理中的典型误用
Peek 常被用来“预读消息头再决定是否处理”,但它不检查空队列:
-
q.Peek()在空时直接抛InvalidOperationException,不是返回null - 安全写法是
if (q.TryPeek(out var head)) { ... }(.NET 6+)或先if (q.Count > 0)
Clear() 也容易误解:
- 它清空逻辑元素,但底层数组不会立即释放——下次
Enqueue仍复用原空间 - 如果消息对象持有大内存(如
byte[]缓冲区),Clear不触发 GC,需手动置null或改用对象池 - 消息缓冲场景中,
Clear很少需要;更常见的是逐个Dequeue处理,或直接新建队列
真正麻烦的不是语法,而是“以为队列很轻量就直接扔进多线程上下文”——消息缓冲一旦出错,丢数据或重复消费都是静默故障,排查成本远高于初期选对类型。


















