網上關於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類型,這里我就不寫了.