摘抄自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