ImmutableArray 不需要加锁,但写操作代价高
ImmutableArrayAdd
、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更合适,但复杂度上升
