多线程场景下读取非线程安全集合是否需要加锁?有何收益?
结论
读取操作必须和写入操作使用同一个lockObj加锁,这是多线程访问非线程安全集合的标准必要操作。
为什么读操作不能省略锁
HashSet<T>属于典型的非线程安全集合,.NET官方文档明确标注:该类的所有公共实例成员都不保障多线程并发场景下的可用性,只要存在任意一个线程执行写入操作(Add/Remove/Clear等修改集合结构的操作),所有读写操作都必须做同步处理,否则会出现以下问题:
- 读到脏数据:写入操作修改
HashSet内部哈希表、链表等结构的过程中,读操作可能读取到中间状态,比如明明已经调用Add成功的元素,Contains却返回false,遍历集合时出现重复元素、跳过元素的异常表现。 - 内存可见性失效:.NET内存模型不保证无同步场景下,写入线程对集合的修改能被读线程感知,读线程可能长期读取CPU缓存中的旧数据,哪怕写入操作已经在主内存中完成很久。
- 触发不可预期的崩溃:并发读写可能破坏
HashSet的内部数据结构一致性,轻则抛出InvalidOperationException、NullReferenceException等运行时异常,重则导致程序死循环、直接崩溃。
读写都加同一把锁的好处
- 操作原子性保障:所有对集合的读写操作串行执行,不会出现读写穿插的情况,完全避免半修改的中间状态被读取。
- 解决内存可见性问题:
lock语法会自动在进入和退出锁区域时添加内存屏障,保证读线程总能获取到之前所有写入操作提交的最新结果。 - 彻底规避未定义行为:只要所有访问都加同一把锁,就不会出现集合内部结构损坏的问题,运行表现完全可预期。
小提示:如果你的场景是读多写少,可以用
ReaderWriterLockSlim替代普通lock优化性能:允许多个读线程同时持有读锁,写入时才升级为独占写锁,比全量独占锁的吞吐量更高,但要注意必须正确处理读写锁的获取和释放逻辑。
内容的提问来源于stack exchange,提问作者Deane
相关产品推荐
相关产品推荐

