c# ImmutableArray 和 List 加锁的性能和场景对比

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

ImmutableArray 不需要加锁,但写操作代价高

ImmutableArray 是不可变结构体,所有“修改”操作(如

Add
SetItem
)都返回新实例,底层复制整个数组。它本身线程安全——读取无需加锁,多个线程同时读不会出错,也不存在竞态。但如果你频繁调用
array.Add(item)
并期望“累积修改”,实际是在反复分配新数组,GC 压力和内存拷贝开销会迅速上升。

常见错误现象:
• 用

for
循环反复
Add
构建集合,性能比
List<t></t>
慢数十倍
• 误以为
ImmutableArray<t></t>
是“高性能无锁集合”,却在写密集场景滥用

适用场景:
• 配置数据、查找表等初始化后极少变更的只读集合
• 函数式风格管道处理(如

.Select(...).Where(...).ToArray()
后转为
ImmutableArray
固化结果)
• 需要值语义比较(
==
.Equals
判断内容是否相同)

List 加锁是唯一线程安全写方式,但粒度很关键

List<t></t>
本身不是线程安全的。多线程同时调用
Add
RemoveAt
或遍历中修改,会触发
InvalidOperationException
(“集合已修改”)或静默数据损坏。必须显式加锁,但锁的范围直接影响吞吐量。

容易踩的坑:
• 对整个

List<t></t>
操作都用同一个
lock(obj)
,变成串行执行,失去并发意义
• 在
foreach
遍历时加锁,但未覆盖全部读路径(比如其他地方有裸读),仍可能遇到
Collection was modified

• 锁对象暴露给外部(如
lock(list)
),引发死锁或意外争用

实操建议:
• 用私有 readonly object 字段做锁对象:

private readonly object _sync = new();

• 写操作(
Add
Clear
)必须锁;纯读(
Count
this[index]
)可不锁,但需接受可能看到“过期”状态
• 若写操作占比 >10%,考虑改用
ConcurrentBag<t></t>
ConcurrentQueue<t></t>
等真正并发集合

性能对比:小数据量差异不大,大数据量写操作 ImmutableArray 明显更慢

在 1000 个元素内,

ImmutableArray<t>.Add</t>
和加锁
List<t>.Add</t>
的单次耗时差距不明显(纳秒级),但前者每次分配新数组,后者只是扩容(摊还 O(1))。当循环添加 10 万次:

var list = new List<int>();
var array = ImmutableArray<int>.Empty;
var sw = Stopwatch.StartNew();
// List + lock
for (int i = 0; i < 100000; i++) {
    lock (_sync) list.Add(i);
}
sw.Restart();
// ImmutableArray
for (int i = 0; i < 100000; i++) {
    array = array.Add(i); // 每次都 new int[i+1] 并 copy
}

后者实际执行约 50 亿次数组元素拷贝(1+2+3+…+100000 ≈ 5e9),而前者仅约 17 次扩容(2→4→8→…→131072),耗时差可达 100 倍以上。

参数差异:

ImmutableArray<t></t>
构造成本低(可由
Array.AsImmutable()
零拷贝创建),但写成本高
List<t></t>
构造无开销,写成本低,但并发写必须自行保证同步

真正该选哪个?看数据生命周期而非“是否要锁”

别纠结“哪个更快加锁”,先问:这个集合的典型使用模式是什么?

ImmutableArray<t></t>
当:
• 数据构建一次,后续只读(如解析 JSON 后的配置项缓存)
• 需要跨线程传递且不允许被意外修改(避免防御性克隆)
• 要利用其 struct 特性减少 GC(但注意:大数组传参会复制)

选加锁

List<t></t>
当:
• 写操作频繁且无法预估总量(如实时日志缓冲区)
• 已有代码重度依赖
List<t></t>
API(索引访问、
Sort
BinarySearch

• 并发度不高(如 2~4 个生产者),锁争用可控

容易被忽略的点:

ImmutableArray<t></t>
.ToBuilder()
返回可变包装器,内部仍用数组,适合“批量构建+固化”场景,比反复
Add
高效得多
• 如果读远多于写,又要求强一致性,
ReaderWriterLockSlim
比简单
lock
更合适,但复杂度上升

相关推荐