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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM