5天玩轉C#並行和多執行緒程式設計 —— 第五天 多執行緒程式設計大總結

雲霏霏發表於2014-11-26

5天玩轉C#並行和多執行緒程式設計系列文章目錄

5天玩轉C#並行和多執行緒程式設計 —— 第一天 認識Parallel

5天玩轉C#並行和多執行緒程式設計 —— 第二天 並行集合和PLinq

5天玩轉C#並行和多執行緒程式設計 —— 第三天 認識和使用Task

5天玩轉C#並行和多執行緒程式設計 —— 第四天 Task進階

5天玩轉C#並行和多執行緒程式設計 —— 第五天 多執行緒程式設計大總結

 

 一、多執行緒帶來的問題

1、死鎖問題 

  前面我們學習了Task的使用方法,其中Task的等待機制讓我們瞬間愛上了它,但是如果我們在呼叫Task.WaitAll方法等待所有執行緒時,如果有一個Task一直不返回,會出現什麼情況呢?當然,如果我們不做出來的話,程式會一直等待下去,那麼因為這一個Task的死鎖,導致其他的任務也無法正常提交,整個程式"死"在那裡。下面我們來寫一段程式碼,來看一下死鎖的情況:

         var t1 = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Task 1 Start running...");
                while(true)
                {
                    System.Threading.Thread.Sleep(1000);
                }
                Console.WriteLine("Task 1 Finished!");
            });
            var t2 = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Task 2 Start running...");
                System.Threading.Thread.Sleep(2000);
                Console.WriteLine("Task 2 Finished!");
            });
            Task.WaitAll(t1,t2);

這裡我們建立兩個Task,t1和t2,t1裡面有個while迴圈,由於條件一直為TRUE,所以他永遠也無法退出。執行程式,結果如下:

可以看到Task2完成了,就是遲遲等不到Task1,這個時候我們按回車是沒有反應的,除非關掉視窗。如果我們在專案中遇到這種情況是令人很糾結的,因為我們也不知道到底發生了什麼,程式就是停在那裡,也不報錯,也不繼續執行。

那麼出現這種情況我們該怎麼處理呢?我們可以設定最大等待時間,如果超過了等待時間,就不再等待,下面我們來修改程式碼,設定最大等待時間為5秒(專案中可以根據實際情況設定),如果超過5秒就輸出哪個任務出錯了,程式碼如下:

           Task[] tasks = new Task[2];
            tasks[0] = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Task 1 Start running...");
                while(true)
                {
                    System.Threading.Thread.Sleep(1000);
                }
                Console.WriteLine("Task 1 Finished!");
            });
           tasks[1] = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Task 2 Start running...");
                System.Threading.Thread.Sleep(2000);
                Console.WriteLine("Task 2 Finished!");
            });
            
            Task.WaitAll(tasks,5000);
            for (int i = 0; i < tasks.Length;i++ )
            {
                if (tasks[i].Status != TaskStatus.RanToCompletion)
                {
                    Console.WriteLine("Task {0} Error!",i + 1);
                }
            }
            Console.Read();

這裡我們將所有任務放到一個陣列裡面進行管理,呼叫Task.WaitAll的一個過載方法,第一個引數是Task[]資料,第二個引數是最大等待時間,單位是毫秒,這裡我們設定為5000及等待5秒鐘,就繼續向下執行。下面我們遍歷Task陣列,通過Status屬性判斷哪些Task沒有完成,然後輸出錯誤資訊。

 

2、SpinLock(自旋鎖)

   我們初識多執行緒或者多工時,第一個想到的同步方法就是使用lock或者Monitor,然而在4.0 之後微軟給我們提供了另一把利器——spinLock,它比重量級別的Monitor具有更小的效能開銷,它的用法跟Monitor很相似,VS給的提示如下:

下面我們來寫一個例子看一下,程式碼如下(關於lock和Monitor的用法就不再細說了,網上資料很多,大家可以看看):

          SpinLock slock = new SpinLock(false);
            long sum1 = 0;
            long sum2 = 0;
            Parallel.For(0, 100000, i =>
            {
                sum1 += i;
            });

            Parallel.For(0, 100000, i =>
            {
                bool lockTaken = false;
                try
                {
                    slock.Enter(ref lockTaken);
                    sum2 += i;
                }
                finally
                {
                    if (lockTaken)
                        slock.Exit(false);
                }
            });

            Console.WriteLine("Num1的值為:{0}", sum1);
            Console.WriteLine("Num2的值為:{0}", sum2);

            Console.Read();

輸出結果如圖:

這裡我們使用了Parallel.For方法來做演示,Parallel.For用起來方便,但是在實際開發中還是儘量少用,因為它的不可控性太高,有點簡單粗暴的感覺,可能帶來一些不必要的"麻煩",最好還是使用Task,因為Task的可控性較好。

slock.Enter方法,解釋如下:

 

3、多執行緒之間的資料同步

  多執行緒間的同步,在用thread的時候,我們常用的有lock和Monitor,上面剛剛介紹了.Net4.0中一個新的鎖——SpinLock(自旋鎖),實際上,我們還可以將任務分成多塊,由多個執行緒一起執行,最後合併多個執行緒的結果,如:求1到100的和,我們分10個執行緒,分別求1~10,......,90~100的和,然後合併十個執行緒的結果。還有就是使用執行緒安全集合,可參加第二天的文章。其實Task的同步機制做已經很好了,如果有特殊業務需求,有執行緒同步問題,大家可一起交流~~

 

 二、Task和執行緒池之間的抉擇

  我們要說的task的知識也說的差不多了,接下來我們開始站在理論上了解下“執行緒池”和“任務”之間的關係,我們要做到知其然,還要知其所以然。不管是說執行緒還是任務,我們都不可避免的要討論下執行緒池,然而在.net 4.0以後,執行緒池引擎考慮了未來的擴充套件性,已經充分利用多核微處理器架構,只要在可能的情況下,我們應該儘量使用task,而不是執行緒池。

   這裡簡要的分析下CLR執行緒池,其實執行緒池中有一個叫做“全域性佇列”的概念,每一次我們使用QueueUserWorkItem的使用都會產生一個“工作項”,然後“工作項”進入“全域性佇列”進行排隊,最後執行緒池中的的工作執行緒以FIFO(First Input First Output)的形式取出,這裡值得一提的是在.net 4.0之後“全域性佇列”採用了無鎖演算法,相比以前版本鎖定“全域性佇列”帶來的效能瓶頸有了很大的改觀。那麼任務委託的執行緒池不光有“全域性佇列”,而且每一個工作執行緒都有”區域性佇列“。我們的第一反應肯定就是“區域性佇列“有什麼好處呢?這裡暫且不說,我們先來看一下執行緒池中的任務分配,如下圖:

執行緒池的工作方式大致如下,執行緒池的最小執行緒數是6,執行緒1~3正在執行任務1~3,當有新的任務時,就會向執行緒池請求新的執行緒,執行緒池會將空閒執行緒分配出去,當執行緒不足時,執行緒池就會建立新的執行緒來執行任務,直到執行緒池達到最大執行緒數(執行緒池滿)。總的來說,只有有任務就會分配一個執行緒去執行,當FIFO十分頻繁時,會造成很大的執行緒管理開銷。

  下面我們來看一下task中是怎麼做的,當我們new一個task的時候“工作項”就會進去”全域性佇列”,如果我們的task執行的非常快,那麼“全域性佇列“就會FIFO的非常頻繁,那麼有什麼辦法緩解呢?當我們的task在巢狀的場景下,“區域性佇列”就要產生效果了,比如我們一個task裡面有3個task,那麼這3個task就會存在於“區域性佇列”中,如下圖的任務一,裡面有三個任務要執行,也就是產生了所謂的"區域性佇列",當任務三的執行緒執行完成時,就會從任務一種的佇列中以FIFO的形式"竊取"任務執行,從而減少了執行緒管理的開銷。這就相當於,有兩個人,一個人幹完了分配給自己的所有活,而另一個人卻還有很多的活,閒的人應該接手點忙的人的活,一起快速完成。

  從上面種種情況我們看到,這些分流和負載都是普通ThreadPool.QueueUserWorkItem所不能辦到的,所以說在.net 4.0之後,我們儘可能的使用TPL,拋棄ThreadPool。

 

這是5天玩轉C#並行和多執行緒程式設計系列的最後一篇了,當然還有很多東西沒說到,如果真的想要玩轉多執行緒,還是要多多努力學習的。大家在學習過程中有什麼問題可以一起交流~~

 

如果大家感覺我的博文對大家有幫助,請推薦支援一把,給我寫作的動力。

 

 作者:雲霏霏

 部落格地址:http://www.cnblogs.com/yunfeifei/

 宣告:本部落格原創文字只代表本人工作中在某一時間內總結的觀點或結論,與本人所在單位沒有直接利益關係。非商業,未授權,貼子請以現狀保留,轉載時必須保留此段宣告,且在文章頁面明顯位置給出原文連線。

 

相關文章