1. 程式人生 > >並行化時要注意的執行緒安全與任務安全

並行化時要注意的執行緒安全與任務安全

在序列程式設計時,操作都是按順序執行的,比如數字從1到100000遞增,就必然的是1、2、3、4……100000。程式碼如下

for (int i = 1; i <= 100000; i++)
            {
                Console.WriteLine(i);   
            }



然而,在並行化程式設計時,因為是並行執行的,所以執行順序會與系統和硬體有關,這樣一來,執行順序就變的不可預知,比如。
Parallel.For(1,100001,(i)=>Console.WriteLine(i));


很明顯,最大的數100000在前面就已經輸出了,如果執行多次,會看到這個結果不是固定的。

因為執行順序的不確定性,所以當我們在多個執行緒或者多個任務中去同時操作一個變數時,就可能引發問題。比如

  int testValue = 0;
            Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 100; i++)
                {
                    testValue++;
                    Console.WriteLine("Add:" + testValue);
                }

            });
            Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 100; i++)
                {
                    testValue--;
                    Console.WriteLine("subtract:" + testValue);
                }
            });
            Console.ReadLine();


在兩個task中執行增加和減少操作,結果變的不可測。而如果我們線上程或者任務中去依賴於這樣的變數作判斷條件時,後果就會變的相當嚴重。這裡,testValue變數是一種非執行緒安全/非任務安全的,如果要對其實現遞增和遞減就得采用原子操作,即Interlocked.Increment和Interlocked.Decrement。

既然有非執行緒安全/非任務安全,自然就有對應的執行緒安全/任務安全,這包含在System.Collections.Concurrent的名稱空間中。ConcurrentQueue、ConcurrentStack、ConcurrentBag、ConcurrentDictionary,這些對應了Queue、Stack、Array/List、Dictionary。

比如我們需要在A執行緒新增物件,又需要在B執行緒中移除物件時,可能道先想到的是List,但List在多執行緒操作會出問題,因為並行執行會導致併發,結果有可能在同一時間內對List進行操作。這時就要考慮使用執行緒安全/任務安全的型別來替代。

總的來說,在多執行緒或者多工的並行化操作時,要優先考慮執行緒安全/任務安全的資料型別,如果一定要用非執行緒安全的資料時,就得增加同步控制(比如原子操作Interlocked、鎖lock、訊號量SemaphoreSlim/Semaphore、倒計事件CountdownEvent、共享事件ManualResetEventSlim/ManualResetEvent、自旋鎖SpinLock等)。