好久不见,马甲哥封闭居家半个月,记录之前遇到的一件小事。
ConcurrentDictionary
All these operations are atomic and are thread-safe with regards to all other operations on the ConcurrentDictionary
之前有个同事就因为这个case背了一个P。
AddOrUpdate(TKey, TValue, Func
GetOrAdd(TKey key, Func
整个过程中涉及与字典直接交互的都用到到精细锁,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.
示例代码:
using System.Collections.Concurrent;
public class Program
{
private static int _runCount = 0;
private static readonly ConcurrentDictionary_dictionary
= new ConcurrentDictionary();
public static void Main(string[] args)
{
var task1 = Task.Run(() => PrintValue("The first value"));
var task2 = Task.Run(() => PrintValue("The second value"));
var task3 = Task.Run(() => PrintValue("The three value"));
var task4 = Task.Run(() => PrintValue("The four value"));
Task.WaitAll(task1, task2, task4,task4);
PrintValue("The five value");
Console.WriteLine($"Run count: {_runCount}");
}
public static void PrintValue(string valueToPrint)
{
var valueFound = _dictionary.GetOrAdd("key",
x =>
{
Interlocked.Increment(ref _runCount);
Thread.Sleep(100);
return valueToPrint;
});
Console.WriteLine(valueFound);
}
}
上面4个线程并发插入字典,每次随机输出,_runCount=4显示工厂类执行4次。
笔者的同事之前就遇到这样的问题,高并发请求频繁创建redis连接,直接打挂了机器。
A: 有一个trick能解决这个问题: valueFactory工厂函数返回Lazy容器.
using System.Collections.Concurrent;
public class Program
{
private static int _runCount2 = 0;
private static readonly ConcurrentDictionary> _lazyDictionary
= new ConcurrentDictionary>();
public static void Main(string[] args)
{
task1 = Task.Run(() => PrintValueLazy("The first value"));
task2 = Task.Run(() => PrintValueLazy("The second value"));
task3 = Task.Run(() => PrintValueLazy("The three value"));
task4 = Task.Run(() => PrintValueLazy("The four value"));
Task.WaitAll(task1, task2, task4, task4);
PrintValue("The five value");
Console.WriteLine($"Run count: {_runCount2}");
}
public static void PrintValueLazy(string valueToPrint)
{
var valueFound = _lazyDictionary.GetOrAdd("key",
x => new Lazy(
() =>
{
Interlocked.Increment(ref _runCount2);
Thread.Sleep(100);
return valueToPrint;
}));
Console.WriteLine(valueFound.Value);
}
}
上面示例,依旧会随机稳定输出,但是_runOut=1表明产值动作只执行了一次、
valueFactory工厂函数返回Lazy容器是一个精妙的trick。
① 工厂函数依旧没进入锁定过程,会多次执行;
② 与最上面的例子类似,只会插入一个Lazy容器(后续线程依旧做double check发现字典key已经有Lazy容器了,会放弃插入);
③ 线程执行Lazy.Value, 这时才会执行创建value的工厂函数;
④ 多个线程尝试执行Lazy.Value, 但这个延迟初始化方式被默认设置为ExecutionAndPublication:不仅以线程安全的方式执行, 而且确保只会执行一次构造函数。
public Lazy(FuncvalueFactory)
:this(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication, useDefaultConstructor: false)
{
}
控制构造函数执行的枚举值 |
描述 |
ExecutionAndPublication[3] |
能确保只有一个线程能够以线程安全方式执行构造函数 |
None |
线程不安全 |
Publication |
并发线程都会执行初始化函数,以先完成初始化的值为准 |
IHttpClientFactory在构建<命名HttpClient,活跃连接Handler>字典时, 也用到了这个技巧,大家自行欣赏DefaultHttpCLientFactory源码[4]。
为解决ConcurrentDictionary GetOrAdd(key, valueFactory) 工厂函数在并发场景下被多次执行的问题:
① valueFactory工厂函数产生Lazy容器;
② 将Lazy容器的值初始化姿势设定为ExecutionAndPublication(线程安全且执行一次)。
两姿势缺一不可。
当前题目:ConcurrentDictionary字典操作竟然不全是线程安全的?
新闻来源:http://www.shufengxianlan.com/qtweb/news20/104820.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联