ConcurrentDictionary字典操作竟然不全是线程安全的?

网站建设4年前发布
28 0 0

好久不见,马甲哥封闭居家半个月,记录之前遇到的一件小事。,ConcurrentDictionary<TKey,TValue>绝大部分api都是线程安全的[1],唯二的例外是接收工厂函数的api:​AddOrUpdate、GetOrAdd,这两个api不是线程安全的,需要引起重视。,All these operations are atomic and are thread-safe with regards to all other operations on the ConcurrentDictionary<TKey,TValue> class. The only exceptions are the methods that accept a delegate, that is, AddOrUpdate and GetOrAdd.,之前有个同事就因为这个case背了一个P。,AddOrUpdate(TKey, TValue, Func<TKey,TValue,TValue> valueFactory);,GetOrAdd(TKey key, Func<TKey, TValue> valueFactory);(注意,包括其他接收工厂委托的重载函数),整个过程中涉及与字典直接交互的都用到到精细锁,valueFactory工厂函数在锁定区外面被执行,因此,这些代码不受原子性约束。,A: 还不是因为微软不相信你能写出健壮的业务代码,未知的业务代码可能造成死锁。,However, delegates for these methods are called outside the locks to avoid the problems that can arise from executing unknown code under a lock. Therefore, the code executed by these delegates is not subject to the atomicity of the operation.,示例代码:,上面4个线程并发插入字典,每次随机输出,_runCount=4显示工厂类执行4次。,20230306013252a969f97096e1a09ca7c7091ff106a2a9fca71a626,笔者的同事之前就遇到这样的问题,高并发请求频繁创建redis连接,直接打挂了机器。,A: 有一个trick能解决这个问题: valueFactory工厂函数返回Lazy容器.,20230306013252f5d541c990d5722a33b455df4d689ce752107e307,上面示例,依旧会随机稳定输出,但是_runOut=1表明产值动作只执行了一次、,valueFactory工厂函数返回Lazy容器是一个精妙的trick。,① 工厂函数依旧没进入锁定过程,会多次执行;,② 与最上面的例子类似,只会插入一个Lazy容器(后续线程依旧做double check发现字典key已经有Lazy容器了,会放弃插入);,③ 线程执行Lazy.Value, 这时才会执行创建value的工厂函数;,④ 多个线程尝试执行Lazy.Value, 但这个延迟初始化方式被默认设置为ExecutionAndPublication:不仅以线程安全的方式执行, 而且确保只会执行一次构造函数。,IHttpClientFactory在构建<命名HttpClient,活跃连接Handler>字典时, 也用到了这个技巧,大家自行欣赏DefaultHttpCLientFactory源码[4]。,为解决ConcurrentDictionary GetOrAdd(key, valueFactory) 工厂函数在并发场景下被多次执行的问题:,① valueFactory工厂函数产生Lazy容器;,② 将Lazy容器的值初始化姿势设定为ExecutionAndPublication(线程安全且执行一次)。,两姿势缺一不可。

© 版权声明

相关文章