一、概述與概念
C#支持通過多線程並行地執行代碼,一個線程有它獨立的執行路徑,能夠與其它的線程同時地運行。一個C#程序開始於一個單線程,這個單線程(也稱為“主線程”)是被CLR和操作系統自動創建的,能夠通過添加額外的線程創建多線程。
下面是個簡單的例子:
class Program01 { static void Main() { Thread t = new Thread(WriteY); t.Start(); while (true) Console.Write("x"); } static void WriteY() { while (true) Console.Write("y"); } }
CLR分配每個線程到它自己的內存堆棧上,來保證局部變量的分離運行。
class Program02 { static void Main() { new Thread(Go).Start(); // 調用Go()方法在一個新線程中 Go(); // 在主線程中調用Go() Console.Read(); } static void Go() { // 聲明和使用一個局部變量'cycles' for (int cycles = 0; cycles < 5; cycles++) Console.Write('?'); } }
其中我們定義一個局部變量,然后主線程和新創建的線程上同時調用這個方法。變量cycles的副本分別在各自內存堆棧中創建,輸出也是一樣的。
當線程引用了一些公用的目標實例時,他們會共享數據:下面是實例:
1 class Program03 2 { 3 bool done; 4 static void Main() 5 { 6 Program03 tt = new Program03(); // 創建一個實例 7 new Thread(tt.Go).Start(); 8 tt.Go(); 9 Console.Read(); 10 } 11 // 注意Go現在是一個實例方法 12 void Go() 13 { 14 if (!done) 15 { 16 done = true; Console.WriteLine("Done"); 17 } 18 } 19 }
因為在相同的實例中,兩個線程都調用了Go(),它們共享了done字段,所以輸出一個“Done”,而不是兩個。
靜態字段提供了另一種在線程間共享數據的方式:
class Program04 { static bool done; // 靜態方法被所有 線程一塊使用 static void Main() { new Thread(Go).Start(); Go(); Console.Read(); } static void Go() { if (!done) { done = true; Console.WriteLine("Done"); } } }
上述的兩個例子輸出實際上是不確定的:它可能打印兩次的“Done”,如果我們在Go方法里面調換代碼順序,這種不確定性更明顯。因為一個線程在判斷if塊的時候,正好另一個線程正在執行輸出語句。這就引出了另一個關鍵的概念——線程安全。
當讀寫公共字段的時候,需要我們我們提供一個排他鎖;
class Program05 { static bool done; static object locker = new object(); static void Main() { new Thread(Go).Start(); Go(); Console.Read(); } static void Go() { lock (locker) { if (!done) { Console.WriteLine("Done"); done = true; } } } }
當兩個線程爭奪一個鎖的時候,一個線程等待,或者被阻止到那個鎖變的可用。在這中情況下,就確保了在同一時刻只有一個線程能進入臨界區。代碼以如此方式在不確定的多線程環境中被叫做線程安全。
二、線程是如何工作的
線程被一個線程協調程序管理着——一個CLR委托給操作系統的函數。線程協調程序確保分配適當的時間給所有活動的線程;其中那些等待或被阻止的線程都是不消耗CPU時間的。
在單核處理器的電腦中,線程協調程序完成一個時間片之后,迅速地在活動線程之間進行切換執行。通常時間片在10毫秒內,要比處理線程切換的消耗(通常在幾微妙區間)大的多。
在多核的電腦中,多線程被實現成混合時間片和真實的並發,不同的線程在不同的CPU上運行。
線程由於外部因素(比如時間片)被中斷被成為被搶占,一個線程在被搶占的那一時刻就失去了對它的控制權。
三、線程VS進程
屬於一個單一的應用程序的所有的線程邏輯被包含在一個進程中,進程指一個應用程序多運行的操作系統單元。
線程與進程有某些相似的地方;比如說進行通常以時間片方式與其它在電腦中運行的進程的方式與一個C#程序線程運行的方式是大致相同。兩者的關鍵區別在於進程彼此是完全隔絕的,線程與運行在相同程序中的其它線程共享內存。一個線程可以在后台讀取數據,而另一個線程可以在前台展現已讀取的數據。
四、多線程的使用場景
何時使用多線程
多線程程序一般用來在后台執行耗時的任務。主線程保持運行,並且工作線程做它的后台工作。在沒有用戶界面的程序里,當一個任務有潛在的耗時(因為它在等待另一台電腦的響應,比如服務器)的實現,多線程就特別有意義。用工作線程完成任務意味着主線程可以立即做其它的事情。
另一個多線程的用途是在方法中完成一個復雜的計算工作:這個方法會在多核的電腦上運行的更快(因為工作量被多個線程分開,使用Environment.ProcessorCount屬性來偵測處理芯片的數量)。
一個C#程序可以通過明確的創建和運行多線程,也可以使用.net framework的暗中使用了多線程的特性——比如BackgroundWorker類、線程池、threading timer、遠程服務器或Web Services 或Asp.net 程序。
何時不要使用多線程
多線程的交互復雜、開發周期長,以及帶來間歇行和非重復性的bugs。頻繁的分配和切換線程時,多線程會增加資源和CPU的開銷。在某些情況下,太多的I/O操作是非常棘手的,一個或兩個工作線程要比有眾多的線程在相同時間執行任務快的多。
五、創建和開始使用多線程
線程用Thread類來創建,通過ThreadStart委托來指明方法先哦那個哪里開始運行。
ThreadStart定義如下:
public delegate void ThreadStart();
調用Start方法后,線程開始運行,線程一直到它所調用的方法返回后結束。
下面的示例通過ThreadStart委托創建多線程:
class ThreadTest { static void Main() { Thread t = new Thread (new ThreadStart (Go)); t.Start(); // 在新線程中運行Go() Go(); // 同時在主線程中運行Go() } static void Go() { Console.WriteLine ("hello!"); }
一個線程可以通過C#堆委托簡短的語法更便利地創建出來:
static void Main() { Thread t = new Thread (Go); // 沒必要明確地使用ThreadStart t.Start(); ... } static void Go() { ... }
在這種情況,ThreadStart被編譯器自動推斷出來,另一個快捷的方式是使用匿名方法來啟動線程:
static void Main() { Thread t = new Thread (delegate() { Console.WriteLine ("Hello!"); }); t.Start(); }
線程有一個IsAlive屬性,在調用Start()之后直到線程結束之前一直為true。
一個線程一旦結束便不能重新開始了。
將數據傳入ThreadStart中
我們想更好地區分開每個線程的輸出結果,讓其中一個線程輸出大寫字母。我們傳入一個狀態 字到Go中來完成整個任務,但我們不能使用ThreadStart委托,因為它不接受參數,所幸的是,.NET framework定義了另一 個版本的委托叫做ParameterizedThreadStart, 它可以接收一個單獨的object類型參數:
public delegate void ParameterizedThreadStart (object obj);
之前的例子看起來是這樣的:
class ThreadTest { static void Main() { Thread t = new Thread (Go); t.Start (true); // == Go (true) Go (false); } static void Go (object upperCase) { bool upper = (bool) upperCase; Console.WriteLine (upper ? "HELLO!" : "hello!"); }
在整個例子中,編譯器自動推斷出ParameterizedThreadStart委托,因為Go方法接收一個單獨的object參數,就像這樣 寫:
Thread t = new Thread (new ParameterizedThreadStart (Go));
t.Start (true)
六、名稱線程
線程可以統統它的Name屬性進行命名,這非常利於調試。線程的名字可以被任何時間設置,但是只能設置一次,重命名會引發異常。
七、前台線程和后台線程
線程默認為前台線程,這意味着任何前台線程在運行都會保持程序存活。C#也支持后台線程,它們不維持程序存活。可以通過IsBackgound屬性控制它的前后台狀態,改變線程從前台到后台不會以任何方式改變它在CPU協調程序中的優先級和狀態。
八、線程優先級
線程的Priority屬性確定了線程相對於其它同一進程的活動的線程擁有多少執行時間,以下是級別:
enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
只有多個線程同時為活動時,優先級才有作用。設置一個線程的優先級為高一些,並不意味着它能執行實時的工作,因為它受限於程序的進程的級別。要執行實時的工作,必 須提升在System.Diagnostics 命名空間下Process的級別,像下面這樣
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
High大體上被認為最高的有用進程級別。
如果一個實時的程序有一個用戶界面,提升進程的級別是不太好的,因為當用戶界面UI過於復雜的時候,界面的更新耗費過多 的CPU時間,拖慢了整台電腦。最理想的方案是使實時工作和用戶界面在不同的進程(擁有不同的優先級)運行,通 過Remoting或共享內存方式進行通信,共享內存需要Win32 API中的 P/Invoking。
九、異常處理
任何創建線程范圍內的try/catch/finally塊,當線程開始執行便不再與其有任何關系。
考慮下面的程序:
public static void Main() { try { new Thread (Go).Start(); } catch (Exception ex) { // 不會在這得到異常 Console.WriteLine ("Exception!"); } } static void Go() { throw null; }
這里try/catch語句一點用也沒有,新創建的線程將引發NullReferenceException異常。因為每個線程有獨立的執行路徑,我們需要在線程處理的方法內加入它們自己的異常處理:
public static void Main() { new Thread (Go).Start(); } static void Go() { try { ... throw null; // 這個異常在下面會被捕捉到 ... } catch (Exception ex) { 記錄異常日志,並且或通知另一個線程 我們發生錯誤 ... }
何線程內的未處理的異常都將導致整個程序關閉,因此try/catch塊需要出現在每個線程進入的方法內。由工作線程拋出的異常,沒有被Application.ThreadException捕捉到。 AppDomain.UnhandledException,這個事件在任何類型 的程序(有或沒有用戶界面)的任何線程有任何未處理的異常觸發。