C#多線程(一)


一、定義與理解

1、定義

線程是操作系統分配CPU時間片的基本單位,每個運行的引用程序為一個進程,這個進程可以包含一個或多個線程。

線程是進程中的執行流程,每個線程可以得到一小段程序的執行時間,在單核處理器中,由於切換線程速度很快因此感覺像是線程同時允許,其實任意時刻都只有一個線程運行,但是在多核處理器中,可以實現混合時間片和真實的並發執行。但是由於操作系統自己的服務或者其他應用程序執行,也不能保證一個進程中的多個線程同時運行。

線程被一個CLR委托給操作系統的進程協調函數管理,確保所有線程都可以被分配適當的執行時間,同時保證在等待或阻止的線程不占用執行時間。

2、理解

線程與進程的關鍵區別是:進程是彼此隔離的,進程是操作系統分配資源的基本單位,而同一個進程中的多個線程是共享該進程內存堆區(Heap)的數據的,可以進行直接的數據共享。但是對於同一進程內的不同線程維護各自的內存棧(Stack),因此各線程的局部變量是隔離的。通過下面的例子可以看出。

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. static void Main(string[] args)  
  2. {  
  3.     Thread t = new Thread(Write);  
  4.     t.Start();  
  5.     Write();  
  6.     Console.ReadKey();  
  7. }  
  8.   
  9. static void Write()  
  10. {  
  11.     for (int i = 0; i < 5; i++)  
  12.         Console.Write("@");  
  13. }  


結果輸出的是10個“@”,在兩個線程中都有局部變量i,是彼此隔離的。但是對於共享的引用變量和靜態數據,多個線程是會產生不可預知的結果的,這里共享的數據也就是“臨界數據”,從而引發了線程安全的概念。

 

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. static bool done;  
  2. static void Main(string[] args)  
  3. {  
  4.     Thread t = new Thread(Write);  
  5.     t.Start();  
  6.     Write();  
  7.     Console.ReadKey();  
  8. }  
  9.   
  10. static void Write()  
  11. {  
  12.     if (!done)  
  13.     {  
  14.         done = true;  
  15.         Console.Write("@");  
  16.     }  
  17. }  


這里輸出的只有一個字符,但是很可能在極少數情況下會出現輸出兩個字符的情況,而且這是不可預知的。但是,對於共享的引用就不會出現這種情況。

 

二、線程使用情形

 

  • 客戶端應用程序保持對用戶的響應:由於某些應用程序的特定需求,多線程程序一般用來執行需要非常耗時的操作,此時使用主線程創建工作線程在后台執行耗時的任務,而主線程保持運行,例如保持與用戶的交互(更新進度條、顯示提示文字等),這樣可以防止由於程序耗時而被操作系統提示“無響應”而被用戶強制關閉進程。
  • 及時處理請求:對於Web應用程序,主線程相應客戶端用戶的請求,返回數據的同時,工作線程從數據庫選出最新數據。這樣可以對某些實時性要求高的應用非常有效,同時可以查詢工作量被單獨線程分開執行,特別是在多核處理器上,可以提高程序的性能。同時對於服務器需要處理多種類型的請求的時候,如ASP.NET、WCF、Remoting等,從而可以實現並發響應。
  • 防止一個線程長時間沒有響應而阻塞CPU來提高效率:例如WebService服務,對於沒有用戶交互界面的訪問,在等待提供webservice服務(比較耗時)的電腦的響應的同時可以執行其他工作,以提高效率。

 

問題:

多線程的問題是使程序中的多個線程的交互變得過於復雜,會帶來較長的開發時間和間歇性或非重復性的bug。同時線程數目不能太多,否則頻繁的分配和切換線程會帶來資源和CPU的開銷,一般有一個到兩個工作線程就足夠。

三、C#中的線程

C#中主要使用Thread類進行線程操作,位於System.Threading命名空間下,提供了一系列進行多線程編程的類和接口,有線程同步和數據訪問的Mutex、Monitor、Interlocked和AutoResetEvent類,以及ThreadPool類和Timer類等。

首先使用new Thread()創建出新的線程,然后調用Start方法使得線程進入就緒狀態,得到系統資源后就執行,在執行過程中可能有等待、休眠、死亡和阻塞四種狀態。正常執行結束時間片后返回到就緒狀態。如果調用Suspend方法會進入等待狀態,調用Sleep或者遇到進程同步使用的鎖機制而休眠等待。具體過程如下圖所示:

Thread類主要用來創建並控制線程,設置線程的狀態、優先級等。創建線程的時候使用ThreadStart委托或者ParameterizedThreadStart委托來執行線程所關聯的部分代碼(也就是工作線程的運行代碼)。

 

Thread類屬性
屬性 說明            
CurrentThread 獲取當前正在運行的線程
IsAlive 獲取當前線程的執行狀態
Name 獲取或設置線程的名稱
Priority 獲取或設置線程的優先級
ThreadState 獲取包含當前線程狀態的值
Thread類常用方法
方法 說明
Abort 調用此方法的線程引發ThreadAbortException
終止線程
Join 阻止調用線程,知道某個線程終止時為止
Resume 繼續已掛起的線程
Sleep 將線程阻止指定的毫秒數
Start 將線程安排被進行執行
Suspent 掛起線程,如果已經掛起則不起作用

 

四、創建與運行設置

1、創建

 

使用Thread類的構造函數創建線程的時候,需要傳遞一個新線程開始執行的代碼塊,提供了使用無參數的TheadStart委托和帶有一個參數的ParameterizedTheadStart委托。他們的定義如下:

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. public delegate void ThreadStart();  
  2. public delegate void ParameterizedThreadStart(object obj);  

任何時候C#使用上述兩個委托中的一個自動進行線程的創建。

 

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. static void Main()  
  2. {  
  3.     Thread t = new Thread(new TheadStart(Go));  
  4.     t.Start();  
  5.     Go();  
  6. }  
  7. static void Go()  
  8. {  
  9.     Console.Write("hello!");  
  10. }  

上述方式不傳遞參數,可以使用new Thead(Go)的方式直接創建,此時C#會在編譯時自動匹配使用的是ThreadStart委托創建的。下面可以進行傳遞參數創建線程。

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. static void Main()  
  2. {  
  3.     Thread t = new Thread(Go);  
  4.     t.Start("hello");  
  5.     Go();  
  6. }  
  7. static void Go(object msg)  
  8. {  
  9.     string message = (string)msg;  
  10.     Console.Write(message);  
  11. }  

 

此時實際在編譯時使用的new Thread(new ParameterizedThreadStart(Go("hello")))創建的,上述使用Start方法傳遞的參數會默認采用這種方式構建。

第二種方法是使用Lambda表達式:

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. new Thread( () => Go("hello") );  

第三種方法是使用匿名方法:

 

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. new Thread( () => {  
  2.     Console.Write("hello world!");  
  3.     ......  
  4. }).Start();  

注意問題:使用Lambda表達式的時候會存在變量捕獲的問題,如果捕獲的變量是共享的,會出現線程不安全的問題。看下面的例子:

 

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. static void Main(string[] args)  
  2. {  
  3.     for (int i = 0; i < 10; i++)  
  4.         new Thread(() => Write(i)).Start();  
  5.     Console.ReadKey();  
  6. }  
  7.   
  8. static void Write(object obj)  
  9. {  
  10.     string msg = Convert.ToString(obj);  
  11.     Console.Write(msg);  
  12. }  

上述由於使用Lambda表達式傳遞參數,在for循環的作用域內,新建的十個線程共享了局部變量i,傳遞進入i參數可能被多個線程已經修改,因此每次輸出結果都是不確定的,兩次結果如下:

 



上述問題,可以使用在循環體內使用一個tmp變量保存每次的變量i值,這樣輸出的就是0到9這十個數。因為使用tmp變量之后的代碼可以用下面的來理解:

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. int i = 0;  
  2. int tmp = i;  
  3. new Thread(()=>Write(tmp)).Start();  
  4.   
  5. int i = 1;  
  6. int tmp = i;  
  7. new Thread(()=>Write(tmp)).Start();  
  8.   
  9. ...  

上述使用Lambda表達式傳遞參數的問題,使用Start方法傳遞參數也會出現這樣的線程不安全的問題,需要使用特殊的線程同步手段進行避免。

 

2、設置

通過使用Thread.CurrentThread屬性獲取正在運行的線程對象。每個線程都有一個Name屬性,可以設置和修改,但是只能設置一次。這樣可在調試窗口看到每個線程的工作狀態,便於調試。

線程有前台和后台之分,可以使用IsBackground屬性設置,但是這個屬性與線程的優先級是沒有關聯的。前台線程只要有一個在運行應用程序就在運行,當沒有前台線程運行后應用程序終止,也就是在任務管理器中的程序一欄中沒有了此程序,但是此時后台線程任然運行直到其完成操作結束,因此在任務管理器的進程一欄中會找到。

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. static void Main(string[] args)  
  2. {  
  3.     Thread.CurrentThread.Name = "main";  
  4.   
  5.     Thread t = new Thread(Go);  
  6.     t.Name = "worker";  
  7.     t.Start();  
  8.   
  9.     Go();  
  10.     Console.ReadKey();  
  11. }  
  12. static void Go()  
  13. {  
  14.     Console.WriteLine("from " + Thread.CurrentThread.Name);  
  15.     Console.WriteLine("background status: " + Thread.CurrentThread.IsBackground.ToString());  
  16. }  


前台或主線程明確等待任何后台線程完成后再結束才是最好的方式,這大多使用Join方式實現,如果某個工作線程無法實現,可以先終止它,如果失敗再拋棄線程,從而與進程一起消亡。

 

線程的優先級使用Priority設置或獲取,只有在運行時才有作用。分為5個級別:

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. enum ThreadPriority{Lowest, BelowNormal , Normal, AboveNormal, Highest}  

線程優先級設置高並不意味着能執行實時的工作,這受限於所屬進程的級別,要執行實時的工作需要提示System.Diagnostics命名空間下的Process級別:

 

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. using (Process p = Process.GetCurrentProcess())  
  2.   p.PriorityClass = ProcessPriorityClass.High;  

設置為High是一個短暫的最高優先級別,如果設置為Realtime,那么將讓操作系統不然該進程被其他進程搶占,因此如果此程序一旦出現故障將耗盡操作系統資源。因此設置為High就是被認為最高和最有用的進程級別了。

 

對於有用戶界面的程序不適合提升進程級別,因為界面UI的更新需要耗費CPU很多時間,從而拖慢電腦。最好的方式是實時工作和用戶界面使用不同的進程,有不同的進程優先級,通過Remoting或者共享內存的方式進行進程通信。

線程執行先運行最高優先級的線程,高優先級的線程執行完之后才開始執行低優先級的線程。

3、休眠

Thread.Sleep(int ms); Thread.Sleep(TimeSpan timeout);

上述方法為Thread類的兩個靜態方法,用來阻止當前線程指定的時間。

4、終止

使用Abort和Join兩個方法實現。Join會等待另一個線程執行完后再執行。而Abort會引發ThreadAbortException異常,同時可以傳遞一個終止的參數信息。

Thread.Abort();或者Thread.Abort(Object  stateInfo)。

5、異常處理

每個線程都有獨立的執行路徑,因此放在try/catch/finally塊中的新線程都與之無關。補救的方式是在每個線程處理的方法中加入自己的異常處理機制。

 

[csharp]  view plain copy print ? 在CODE上查看代碼片 派生到我的代碼片
 
  1. static void Main(string[] args)  
  2. {  
  3.     try  
  4.     {  
  5.         new Thread(Go).Start();  
  6.     }  
  7.     catch (Exception ex)  
  8.     {  
  9.         Console.Write(ex.Message);  
  10.     }  
  11.       
  12.     Console.ReadKey();  
  13. }  
  14. static void Go()  
  15. {  
  16.      try  
  17.     {  
  18.           throw null;  
  19.     }catch(Exception e){  
  20.           Console.Write(e.Message);  
  21.     }  
  22. }  

上述處理過程在單獨的線程運行中進行異常處理是可以被捕獲到的。同時任何線程內的未處理的異常都會導致整個程序關閉,對於WPF和WinForm程序中的全局異常僅僅在主界面線程執行,對於工作線程的異常需要手動處理。有三種情況可以不用處理工作線程的異常:異步委托、BackgroundWroker、Task Parallel Library。

 

(后續繼續探秘)

參考:http://www.albahari.com/threading/


免責聲明!

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



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