1. 程式人生 > >C# 多執行緒之List的執行緒安全問題

C# 多執行緒之List的執行緒安全問題

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