锁下生成Dictionary不可变副本后迭代是否线程安全?附代码求证
代码线程安全性分析
你的方案是线程安全的
你当前的实现是线程安全的,核心原因在于:
GetServices方法通过ReaderWriterLockSlim的读锁,保证了复制_dictionary.Values到ImmutableList的过程中,不会有任何写入操作干扰——因为GetOrAdd里的写操作必须获取写锁,而读锁存在时写锁会处于等待状态,所以复制出来的ImmutableList是字典在某个时刻的完整、一致的快照。- 释放锁后遍历
ImmutableList并调用异步方法,此时即便其他线程修改了原_dictionary,也不会影响已经拿到的ImmutableList——因为它是不可变的,遍历过程不会被打断,拿到的每个Service实例也是稳定的。
注意GetOrAdd方法的两处问题
你的GetOrAdd有两个容易踩坑的地方:
- 返回值错误:写锁里创建了新的
Service并添加到字典,但返回的是之前TryGetValue得到的service(此时是null),应该返回新创建的实例。 - 缺少二次检查:在升级读锁之后、进入写锁之前,必须再次检查key是否存在——因为这段间隙里,可能有其他线程也拿到了升级读锁并完成了写入,不检查会导致重复添加元素。
修正后的GetOrAdd应该是这样:
public Service GetOrAdd(string key) { lockObject.EnterUpgradeableReadLock(); try { if (_dictionary.TryGetValue(key, out var service)) { return service; } lockObject.EnterWriteLock(); try { // 二次检查,避免多线程竞争导致重复添加 if (!_dictionary.TryGetValue(key, out service)) { service = new Service(); _dictionary.Add(key, service); } return service; } finally { lockObject.ExitWriteLock(); } } finally { lockObject.ExitUpgradeableReadLock(); } }
普通Dictionary的可行性
你提到因为要维护两个集合,GetOrAdd仍需保留锁,所以想用普通Dictionary是完全可行的:
- 只要所有对
_dictionary的读写操作都通过ReaderWriterLockSlim正确加锁:读操作加读锁,写操作加写锁,就能保证Dictionary本身的线程安全——毕竟Dictionary本身不是线程安全的,必须靠外部锁保护所有访问。 - 相比
ConcurrentDictionary,这种方式在需要保证两个集合一致性的场景下更可控:ConcurrentDictionary的原子操作只能覆盖自身,多集合的一致性还是需要额外加锁,反而不如用普通Dictionary配合统一的锁来得直接。
内容的提问来源于stack exchange,提问作者yaskovdev
相关产品推荐
相关产品推荐

