c# ConcurrentBag 和 List 加 lock 的区别

来源:这里教程网 时间:2026-02-21 17:38:38 作者:

ConcurrentBag 不用 lock 就线程安全,但遍历很贵

它内部用的是每个线程私有本地包(ThreadLocal bag)+ 全局共享包的混合结构,添加(

Add
)和尝试取(
TryTake
)基本不争抢,所以高并发写入时比
lock
包裹的
List<t></t>
快很多。但注意:
foreach
遍历
ConcurrentBag<t></t>
会先拷贝全部元素到一个临时
List<t></t>
,再枚举——这意味着每次遍历都触发一次内存分配 + O(n) 拷贝。如果你的场景是“多线程狂塞、单线程最后扫一遍”,那遍历前手动转成
List<t></t>
更划算:

var snapshot = new List<Product>(bag); // 一次性拷贝
foreach (var p in snapshot)
{
    Console.WriteLine(p.Name);
}

lock(List) 简单可控,但锁粒度大、易成瓶颈

object
锁住整个
List<t></t>
实例,所有读写(
Add
RemoveAt
Count
、甚至
foreach
)都排队执行。好处是语义清晰、调试方便;坏处是:哪怕只是读
Count
,也要等前面的写操作释放锁;多个线程同时调用
Add
会严重串行化。

别在
lock
块里做耗时操作(比如 IO、网络请求),否则锁持有时间拉长,拖垮整体吞吐
不要用
list
本身当锁对象(
lock(list)
),它可能被外部修改或设为
null
,推荐用专用
private readonly object _lock = new object();
List<t></t>
ForEach
方法不是线程安全的——即使加了
lock
,遍历时若其他线程正修改,仍可能抛
InvalidOperationException

选哪个?看你的读写比例和访问模式

不是“ConcurrentBag 一定比 lock(List) 快”,而是看实际行为:

写远多于读(如日志缓冲、事件暂存)→
ConcurrentBag
明显优势
读多写少(如配置缓存、只偶尔更新的元数据)→
ReaderWriterLockSlim
+
List<t></t>
可能更优
需要按索引随机访问(
list[i]
)、频繁中间插入/删除 →
ConcurrentBag
不支持,只能换思路(比如改用
ConcurrentDictionary<int t></int>
模拟索引)或坚持
lock
必须保持插入顺序且 FIFO 处理 → 别用
ConcurrentBag
(它是无序的),改用
ConcurrentQueue<t></t>

容易被忽略的坑:ConcurrentBag 的“无序”不是 bug,是设计

ConcurrentBag
不保证任何顺序:
Add
TryTake
的结果取决于线程本地包状态和全局包竞争,同一个线程连续
Add
两个元素,
TryTake
也可能先拿到后一个。如果你依赖顺序(比如任务队列、流水线阶段),用它就埋了隐性 bug。这时候宁可多花点性能成本,也该选
ConcurrentQueue<t></t>
或带锁的
List<t></t>
+ 手动维护索引。

另外,

ConcurrentBag
ToArray()
ToList()
同样要全量拷贝,别在热路径反复调用。

相关推荐