使用Lazy使ConcurrentDictionary的GetOrAdd方法线程安全
摘抄自Making ConcurrentDictionary GetOrAdd thread safe using Lazy
普通使用
private static int runCount = 0;
private static readonly ConcurrentDictionary<string, string> cache
= new ConcurrentDictionary<string, string>();
public static void Run()
{
Task task1 = Task.Run(() => ShowValue("第一个值"));
Task task2 = Task.Run(() => ShowValue("第二个值"));
Task.WaitAll(task1, task2);
ShowValue("第三个值");
Console.WriteLine($"总共运行: {runCount}");
}
public static void ShowValue(string value)
{
string valueFound = cache.GetOrAdd(
key: "key",
valueFactory: _ =>
{
Interlocked.Increment(ref runCount);
Thread.Sleep(10);
return value;
});
Console.WriteLine(valueFound);
}
runCount
计数valueFactory
执行了多少次
运行这个程序会产生两个输出之一,这取决于线程被调度的顺序
第一个值
第一个值
第一个值
总共运行: 2
或者
第二个值
第二个值
第二个值
总共运行: 2
调用GetOrAdd
时始终会得到相同的值,具体取决于哪个线程先返回
但是,委托正在两个异步调用上运行,所以_runCount=2
因为在第二次调用运行之前,该值尚未从第一次调用中存储
执行过程可能如下所示:
线程 A 为键key
在字典上调用GetOrAdd
,但没有找到它,因此开始调用valueFactory
线程 B 还为键key
调用字典上的GetOrAdd
。线程 A 还没有完成,所以没有找到现有的值,线程 B 也开始调用valueFactory
线程 A 完成其调用,并将值第一个值
返回给ConcurrentDictionary。字典检查key
仍然没有值,并插入新的KeyValuePair。最后,它将第一个值
返回给调用者
线程 B 完成其调用并将值第二个值
返回给ConcurrentDictionary。字典看到线程 A 存储的key
的值,因此它丢弃它创建的值并使用该值,将值返回给调用者
线程 C 调用GetOrAdd
并发现key
的值已经存在,因此返回该值,而无需调用valueFactory
使用Lazy
只需要改动cache
和valueFactory
即可
private static readonly ConcurrentDictionary<string, Lazy<string>> cache
= new ConcurrentDictionary<string, Lazy<string>>();
var valueFound = cache.GetOrAdd(
key: "key",
valueFactory: _ => new Lazy<string>(
() =>
{
Interlocked.Increment(ref runCount);
Thread.Sleep(100);
return value;
})
);
这样,runCount
计数为1
执行过程如下所示:
线程 A 为键key
在字典上调用GetOrAdd
但没有找到它,因此开始调用valueFactory
线程 B 还为键key
调用字典上的GetOrAdd
。线程 A 还没有完成,所以没有找到现有的值,线程 B 也开始调用valueFactory
线程 A 完成它的调用,返回一个未初始化的Lazy<string>
对象。Lazy<string>
中的委托此时尚未运行,我们刚刚创建了Lazy<string>
容器。字典检查key
仍然没有值,因此插入 Lazy
线程 B 完成它的调用,类似地返回一个未初始化的Lazy<string>
对象。和以前一样,字典看到线程 A 存储的key
的Lazy<string>
对象,因此它丢弃它刚刚创建的Lazy<string>
并使用线程 A 存储的对象,将其返回给调用者
线程 A 调用Lazy<string>.Value
。这以线程安全的方式调用提供的委托,这样如果它被两个线程同时调用,它将只运行一次委托
线程 B 调用Lazy<string>.Value
。这是线程 A 刚刚初始化的同一个Lazy<string>
对象(请记住,字典可确保您始终获得相同的值。)如果线程 A 仍在运行初始化委托,则线程 B 将阻塞,直到它完成并且可以访问结果。我们只是得到了最终的返回字符串,而没有第二次调用委托。这就是我们需要的一次性行为
线程 C 调用GetOrAdd
并发现key
的Lazy<string>
对象已经存在,因此返回值,而无需调用valueFactory
。Lazy
示例代码
学习技术最好的文档就是【官方文档】,没有之一。
还有学习资料【Microsoft Learn】、【CSharp Learn】、【My Note】。
如果,你认为阅读这篇博客让你有些收获,不妨点击一下右下角的【推荐】按钮。
如果,你希望更容易地发现我的新博客,不妨点击一下【关注】。