C# 多執行緒之List的執行緒安全問題
阿新 • • 發佈:2018-12-09
網上關於List的執行緒安全問題將的很少,所以自己實驗了一把,發現確實是執行緒不安全的.所以當你在進行多執行緒程式設計中使用了共享的List集合,必須對其進行執行緒安全處理.
List的Add方法是執行緒不安全的,List的原始碼中的Add方法,使用了每次噹噹前的元素達到上限,通過建立一個新的陣列例項,並給長度翻倍的操作.如果單執行緒操作不會有問題,直接擴容,然後繼續往裡面加值。下面是List的Add方法和核心邏輯.
也就是說,當多個執行緒同時新增元素,且剛好它們都執行到了擴容這個階段,當一個執行緒擴大了這個陣列的長度,且進行了+1操作後,另外一個執行緒剛好也在執行擴容的操作,這個時候它給Capacity的值設為2048,但是另外一個執行緒已經將this._size設為2049了,所以這個時候就報異常了.當然不止這一個問題,還有Copy的時候也會出問題,如果裡面的元素過多,另外一個執行緒拿到空值的機率很大.
程式碼重現:
class Program { static List<long> list = new List<long>(); static void Main(string[] args) { var t = Task.Run(() => { var tf = new TaskFactory(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.AttachedToParent);var childTasks = new Task[] { tf.StartNew(()=>Task_0()), tf.StartNew(()=>Task_1()), tf.StartNew(()=>Task_2()) }; var tfTask=tf.ContinueWhenAll(childTasks, completedTasks => completedTasks.Where(w => !w.IsFaulted && !w.IsCanceled), TaskContinuationOptions.None); tfTask.ContinueWith(task=> { var a = list; }); }); Console.ReadKey(); } static void Task_0() { for (var i = 0; i < 1000000; i++) { list.Add(i); } } static void Task_1() { for (var i = 0; i < 1000000; i++) { list.Add(i); } } static void Task_2() { for (var i = 0; i < 1000000; i++) { list.Add(i); } } }
多跑幾次這段程式碼,你幾乎可以所有可能出現的多執行緒資源爭用異常.
解決方案:給Add方法加鎖,程式碼如下:
class Program { static object lockObj = new object(); static List<long> list = new List<long>(); static void Main(string[] args) { var t = Task.Run(() => { var tf = new TaskFactory(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.AttachedToParent); var childTasks = new Task[] { tf.StartNew(()=>Task_0()), tf.StartNew(()=>Task_1()), tf.StartNew(()=>Task_2()) }; var tfTask=tf.ContinueWhenAll(childTasks, completedTasks => completedTasks.Where(w => !w.IsFaulted && !w.IsCanceled), TaskContinuationOptions.None); tfTask.ContinueWith(task=> { var a = list; }); }); Console.ReadKey(); } static void Task_0() { for (var i = 0; i < 1000000; i++) { lock (lockObj) { list.Add(i); } } } static void Task_1() { for (var i = 0; i < 1000000; i++) { lock (lockObj) { list.Add(i); } } } static void Task_2() { for (var i = 0; i < 1000000; i++) { lock (lockObj) { list.Add(i); } } } }
ok,解決了問題,當然這不是最好的解決方案,你完全可以通過介面卡模式,去擴充套件一個執行緒安全的List型別,這裡我就不寫了.