使用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

只需要改动cachevalueFactory即可

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,最后,将Lazy返回给调用者
线程 B 完成它的调用,类似地返回一个未初始化的Lazy<string>对象。和以前一样,字典看到线程 A 存储的keyLazy<string>对象,因此它丢弃它刚刚创建的Lazy<string>并使用线程 A 存储的对象,将其返回给调用者
线程 A 调用Lazy<string>.Value。这以线程安全的方式调用提供的委托,这样如果它被两个线程同时调用,它将只运行一次委托
线程 B 调用Lazy<string>.Value。这是线程 A 刚刚初始化的同一个Lazy<string>对象(请记住,字典可确保您始终获得相同的值。)如果线程 A 仍在运行初始化委托,则线程 B 将阻塞,直到它完成并且可以访问结果。我们只是得到了最终的返回字符串,而没有第二次调用委托。这就是我们需要的一次性行为
线程 C 调用GetOrAdd并发现keyLazy<string>对象已经存在,因此返回值,而无需调用valueFactory。Lazy已经被初始化,所以直接返回结果值

示例代码

ConcurrentDictionaryTest

posted @ 2022-03-30 19:27  Lulus  阅读(690)  评论(0编辑  收藏  举报