当前位置: 首页 > 科技观察

ConcurrentDictionary字典操作不都是线程安全的吗?

时间:2023-03-13 16:34:01 科技观察

好久不见,马家哥在家呆了半个月,记录下之前发生在我身上的一件小事。ConcurrentDictionary的大多数API都是线程安全的[1]。唯一的例外是接收工厂函数的API:AddOrUpdate和GetOrAdd。这两个API都不是线程安全的,需要注意。所有这些操作都是原子的,并且对于ConcurrentDictionary类上的所有其他操作都是线程安全的。唯一的例外是接受委托的方法,即AddOrUpdate和GetOrAdd。之前有个同事因为这个case后面带个P。AddOrUpdate(TKey,TValue,FuncvalueFactory);GetOrAdd(TKeykey,FuncvalueFactory);(注意,包括其他接收工厂委托的重载函数)整个过程涉及到字典精细锁直接交互,而valueFactory工厂函数是在锁区外执行的,所以这些代码不受原子约束。Q1:valueFactory工厂函数不在锁定范围内,为什么不在锁定范围内?答:这并不是因为微软不相信你可以编写健壮的业务代码。未知的业务代码可能会导致死锁。但是,这些方法的委托是在锁外调用的,以避免在锁下执行未知代码时可能出现的问题。因此,这些委托执行的代码不受操作的原子性约束。Q2:带来的效果?valueFactory工厂函数可以执行多次。虽然会执行多次,但是inserted的值是固定的,inserted的值取决于哪个线程先插入字典。Q3:如何实现一列值的随机稳定输出?A:源代码已经做了双重检查[2]。后续线程通过工厂类创建一个值后,会再次查字典,如果找到已经存在的值,则丢弃自己创建的值。示例代码:usingSystem.Collections.Concurrent;publicclassProgram{privatestaticint_runCount=0;privatestaticreadonlyConcurrentDictionary_dictionary=newConcurrentDictionary();publicstaticvoidMain(string[]args){vartask1=Task.Run(()=>PrintValue("第一个值"));vartask2=Task.Run(()=>PrintValue("第二个值"));vartask3=Task.Run(()=>PrintValue("三个值"));vartask4=Task.Run(()=>PrintValue("四个值"));Task.WaitAll(task1,task2,task4,task4);PrintValue("五个值");Console.WriteLine($"运行次数:{_runCount}");}publicstaticvoidPrintValue(stringvalueToPrint){varvalueFound=_dictionary.GetOrAdd("key",x=>{Interlocked.Increment(ref_runCount);Thread.Sleep(100);返回值打印;});控制台.WriteLine(valueFound);}}以上4个线程并发插入字典,每次随机输出,_runCount=4说明工厂类执行了4次Q4:如果工厂输出值的成本很高,没有更多的Firstcreation,如何实现?笔者的同事之前就遇到过这样的问题。高并发请求频繁创建redis连接,直接挂机。A:有一个技巧可以解决这个问题:valueFactory工厂函数返回Lazy容器.usingSystem.Collections.Concurrent;publicclassProgram{privatestaticint_runCount2=0;privatestaticreadonlyConcurrentDictionary>_lazyDictionary=newConcurrentDictionary>();publicstaticvoidMain(string[]args){task1=Task.Run(()=>PrintValueLazy("第一个值"));task2=Task.Run(()=>PrintValueLazy("第二个值"));task3=Task.Run(()=>PrintValueLazy("三个值"));task4=Task.Run(()=>PrintValueLazy("四个值"));Task.WaitAll(task1,task2,task4,task4);PrintValue("五个值");Console.WriteLine($"运行次数:{_runCount2}");}publicstaticvoidPrintValueLazy(stringvalueToPrint){varvalueFound=_lazyDictionary.GetOrAdd("key",x=>newLazy(()=>{Interlocked.Increment(ref_runCount2);线程.睡眠(100);返回值打印;}));Console.WriteLine(valueFound.Value);}}上面的例子还是会随机稳定输出,但是_runOut=1表示输出值动作只执行一次,valueFactory工厂函数返回Lazy容器是一个精妙的技巧①工厂函数还是没有进入加锁过程并且会被执行多次;②和上面的例子类似,只会插入一个Lazy容器(后续线程还是会做doublecheck,发现dictionarykey已经有一个Lazycontainer,会放弃插入);③线程执行Lazy.Value,然后执行创建值的工厂函数;④多个线程尝试执行Lazy.Value,但是这种惰性初始化方法默认设置为ExecutionAndPublication:不仅以线程安全的方式执行,而且保证了构造函数只会执行一次。publicLazy(FuncvalueFactory):this(valueFactory,LazyThreadSafetyMode.ExecutionAndPublication,useDefaultConstructor:false){}控制构造函数执行的枚举值说明ExecutionAndPublication[3]可以保证在一个线程中只有一个线程可以执行构造函数线程安全方式None线程不安全Publication并发线程会执行初始化函数,以先初始化的值为准。IHttpClientFactory在构造字典时也使用了这种技术。您可以欣赏DefaultHttpCLientFactory[4]的源码。https://andrewlock.net/making-getoradd-on-concurrentdictionary-thread-safe-using-lazy/针对并发场景下多次执行ConcurrentDictionaryGetOrAdd(key,valueFactory)工厂函数的问题总结:①valueFactoryfactory函数生成一个Lazy容器;②设置Lazy容器的值初始化姿势为ExecutionAndPublication(线程安全,执行一次)。两种姿势缺一不可。