ConcurrentDictionary字典操作竟然不全是執行緒安全的?
好久不見,馬甲哥封閉居家半個月,記錄之前遇到的一件小事。
ConcurrentDictionary<TKey,TValue>絕大部分api都是執行緒安全的[1],唯二的例外是接收工廠函式的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);(注意,包括其他接收工廠委託的過載函式)
整個過程中涉及與字典直接互動的都用到到精細鎖,valueFactory工廠函式在鎖定區外面被執行,因此,這些程式碼不受原子性約束。
Q1: 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.
Q2:帶來的效果?
- valueFactory工廠函式可能會多次執行
- 雖然會多次執行, 但插入的值固定是一個,插入的值取決於哪個執行緒率先插入字典。
Q3: 怎麼做到的隨機穩定輸出一列值?A:原始碼做了double check[2]了,後續執行緒通過工廠類建立值後,會再次檢查字典,發現已有值,會丟棄自己建立的值。
示例程式碼:
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次。
Q4:如果工廠產值的代價很大,不允許多次建立,如何實現?
筆者的同事之前就遇到這樣的問題,高併發請求頻繁建立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容器是一個精妙的trick。
① 工廠函式依舊沒進入鎖定過程,會多次執行;
② 與最上面的例子類似,只會插入一個Lazy容器(後續執行緒依舊做double check發現字典key已經有Lazy容器了,會放棄插入);
③ 執行緒執行Lazy.Value, 這時才會執行建立value的工廠函式;
④ 多個執行緒嘗試執行Lazy.Value, 但這個延遲初始化方式被預設設定為ExecutionAndPublication:不僅以執行緒安全的方式執行, 而且確保只會執行一次建構函式。
public Lazy(Func<T> valueFactory) :this(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication, useDefaultConstructor: false) { }
控制建構函式執行的列舉值 |
描述 |
ExecutionAndPublication[3] |
能確保只有一個執行緒能夠以執行緒安全方式執行建構函式 |
None |
執行緒不安全 |
Publication |
併發執行緒都會執行初始化函式,以先完成初始化的值為準 |
IHttpClientFactory在構建<命名HttpClient,活躍連線Handler>字典時, 也用到了這個技巧,大家自行欣賞DefaultHttpCLientFactory原始碼[4]。
- http://andrewlock.net/making-getoradd-on-concurrentdictionary-thread-safe-using-lazy/
總結
為解決ConcurrentDictionary GetOrAdd(key, valueFactory) 工廠函式在併發場景下被多次執行的問題:
① valueFactory工廠函式產生Lazy容器;
② 將Lazy容器的值初始化姿勢設定為ExecutionAndPublication(執行緒安全且執行一次)。
兩姿勢缺一不可。
- Spring中實現非同步呼叫的方式有哪些?
- 帶引數的全型別 Python 裝飾器
- 整理了幾個Python正則表示式,拿走就能用!
- SOLID:開閉原則Go程式碼實戰
- React中如何引入CSS呢
- 一個新視角:前端框架們都卷錯方向了?
- 編碼中的Adapter,不僅是一種設計模式,更是一種架構理念與解決方案
- 手寫程式語言-遞迴函式是如何實現的?
- 一文搞懂模糊匹配:定義、過程與技術
- 新來個阿里 P7,僅花 2 小時,做出一個多執行緒永動任務,看完直接跪了
- Puzzlescript,一種開發H5益智遊戲的引擎
- @Autowired和@Resource到底什麼區別,你明白了嗎?
- CSS transition 小技巧!如何保留 hover 的狀態?
- React如此受歡迎離不開這4個主要原則
- LeCun再炮轟Marcus: 他是心理學家,不是搞AI的
- Java保證執行緒安全的方式有哪些?
- 19個殺手級 JavaScript 單行程式碼,讓你看起來像專業人士
- Python 的"self"引數是什麼?
- 別整一坨 CSS 程式碼了,試試這幾個實用函式
- 再有人問你什麼是MVCC,就把這篇文章發給他!