好久不見,馬甲哥封閉居家半個月,記錄之前遇到的一件小事。
ConcurrentDictionary<TKey,TValue>絕大部分api都是執行緒安全且原子性的,
唯二的例外是接收工廠委託的api:AddOrUpdate
、GetOrAdd
,這兩個api不是原子性的,需要引起重視。
All these operations are atomic and are thread-safe with regards to all other operations on the ConcurrentDictionary<TKey,TValue> class. The only exceptions are the methods that accept a delegate, that is, AddOrUpdate and GetOrAdd.
之前有個同事就因為這個case背了一個P。
AddOrUpdate(TKey, TValue, Func<TKey,TValue,TValue> valueFactory);
GetOrAdd(TKey key, Func<TKey, TValue> 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.
Q3: 怎麼做到的?
A: 原始碼做了double check了,後續執行緒通過工廠類建立值後,會再次檢查字典,發現已有值,會丟棄自己建立的值。
範例程式碼:
using System.Collections.Concurrent;
public class Program
{
private static int _runCount = 0;
private static readonly ConcurrentDictionary<string, string> _dictionary
= new ConcurrentDictionary<string, string>();
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<string, Lazy<string>> _lazyDictionary
= new ConcurrentDictionary<string, Lazy<string>>();
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<string>(
() =>
{
Interlocked.Increment(ref _runCount2);
Thread.Sleep(100);
return valueToPrint;
}));
Console.WriteLine(valueFound.Value);
}
}
上面範例,依舊會穩定隨機輸出,但是_runOut=1
表明產值動作只執行了一次、
valueFactory工廠函數返回Lazy
① 工廠函數依舊沒進入鎖定過程,會多次執行;
② 與最上面的例子類似,只會插入一個Lazy容器(後續執行緒依舊做double check發現字典key已經有Lazy容器了,會放棄插入);
③ 執行緒執行Lazy
④ 多個執行緒嘗試執行LazyExecutionAndPublication
:
不僅以執行緒安全的方式執行, 而且確保只會執行一次建構函式。
public Lazy(Func<T> valueFactory)
:this(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication, useDefaultConstructor: false)
{
}
控制建構函式執行的列舉值 | 描述 |
---|---|
ExecutionAndPublication | 能確保只有一個執行緒能夠以執行緒安全方式執行建構函式 |
None | 執行緒不安全 |
Publication | 並行執行緒都會執行初始化函數,以先完成初始化的值為準 |
IHttpClientFactory
在構建<命名HttpClient,活躍連線Handler>字典時, 也用到了這個技巧,大家自行欣賞DefaultHttpCLientFactory原始碼。
為解決ConcurrentDictionary GetOrAdd(key, valueFactory) 工廠函數在並行場景下被多次執行的問題。
① valueFactory工廠函數產生Lazy容器
② 將Lazy容器的值初始化姿勢設定為ExecutionAndPublication
(執行緒安全且執行一次)。
兩姿勢缺一不可。
本文來自部落格園,作者:{有態度的馬甲},轉載請註明原文連結:https://www.cnblogs.com/JulianHuang/p/16698976.html
歡迎關注我的原創技術、職場公眾號, 加好友談天說地,一起進化