网上关于List的线程安全问题将的很少,所以自己实验了一把,发现确实是线程不安全的.所以当你在进行多线程编程中使用了共享的List集合,必须对其进行线程安全处理.
List的Add方法是线程不安全的,List的源码中的Add方法,使用了每次当当前的元素达到上限,通过创建一个新的数组实例,并给长度翻倍的操作.如果单线程操作不会有问题,直接扩容,然后继续往里面加值。下面是List的Add方法和核心逻辑.


也就是说,当多个线程同时添加元素,且刚好它们都执行到了扩容这个阶段,当一个线程扩大了这个数组的长度,且进行了+1操作后,另外一个线程刚好也在执行扩容的操作,这个时候它给Capacity的值设为2048,但是另外一个线程已经将this._size设为2049了,所以这个时候就报异常了.当然不止这一个问题,还有Copy的时候也会出问题,如果里面的元素过多,另外一个线程拿到空值的几率很大.
代码重现:
1class Program 2 { 3 static List<long> list = new List<long>(); 4 static void Main(string[] args) 5 { 6 var t = Task.Run(() => 7 { 8 var tf = new TaskFactory(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.AttachedToParent); 9 var childTasks = new Task[] 10 { 11 tf.StartNew(()=>Task_0()), 12 tf.StartNew(()=>Task_1()), 13 tf.StartNew(()=>Task_2()) 14 }; 15 var tfTask=tf.ContinueWhenAll(childTasks, completedTasks => completedTasks.Where(w => !w.IsFaulted && !w.IsCanceled), TaskContinuationOptions.None); 16 tfTask.ContinueWith(task=> 17 { 18 var a = list; 19 }); 20 }); 21 Console.ReadKey(); 22 } 23 24 static void Task_0() 25 { 26 for (var i = 0; i < 1000000; i++) 27 { 28 list.Add(i); 29 } 30 } 31 32 static void Task_1() 33 { 34 for (var i = 0; i < 1000000; i++) 35 { 36 list.Add(i); 37 } 38 } 39 40 static void Task_2() 41 { 42 for (var i = 0; i < 1000000; i++) 43 { 44 list.Add(i); 45 } 46 } 47 }

多跑几次这段代码,你几乎可以重现所有可能出现的多线程资源争用异常.
解决方案:给Add方法加锁,代码如下:
1class Program 2 { 3 4 static object lockObj = new object(); 5 static List<long> list = new List<long>(); 6 static void Main(string[] args) 7 { 8 var t = Task.Run(() => 9 { 10 var tf = new TaskFactory(TaskCreationOptions.AttachedToParent, TaskContinuationOptions.AttachedToParent); 11 var childTasks = new Task[] 12 { 13 tf.StartNew(()=>Task_0()), 14 tf.StartNew(()=>Task_1()), 15 tf.StartNew(()=>Task_2()) 16 }; 17 var tfTask=tf.ContinueWhenAll(childTasks, completedTasks => completedTasks.Where(w => !w.IsFaulted && !w.IsCanceled), TaskContinuationOptions.None); 18 tfTask.ContinueWith(task=> 19 { 20 var a = list; 21 }); 22 }); 23 Console.ReadKey(); 24 } 25 26 static void Task_0() 27 { 28 for (var i = 0; i < 1000000; i++) 29 { 30 lock (lockObj) 31 { 32 list.Add(i); 33 } 34 } 35 } 36 37 static void Task_1() 38 { 39 for (var i = 0; i < 1000000; i++) 40 { 41 lock (lockObj) 42 { 43 list.Add(i); 44 } 45 } 46 } 47 static void Task_2() 48 { 49 for (var i = 0; i < 1000000; i++) 50 { 51 lock (lockObj) 52 { 53 list.Add(i); 54 } 55 } 56 } 57 }
ok,解决了问题,当然这不是最好的解决方案,你完全可以通过适配器模式,去扩展一个线程安全的List类型,这里我就不写了.