5天玩轉C#並行和多線程編程 —— 第五天 多線程編程大總結


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/

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM