線程用Thread類來創建, 通過ThreadStart委托來指明方法從哪裡開始運行。ThreadStart的聲明如下:
public delegate void ThreadStart();
調用Start方法後,線程開始運行,直到它所調用的方法返回後結束。
class ThreadTest { static void Main() { Thread t = new Thread (new ThreadStart (Go)); t.Start(); 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 ThreadDemo { 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);
ParameterizedThreadStart的特性是在使用之前我們必需對我們想要的類型(這裡是bool)進行裝箱操作,並且它只能接收一個參數。
一個替代方案是使用一個匿名方法調用一個普通的方法如下:
static void Main() { Thread t = new Thread (delegate() { WriteText ("Hello"); }); t.Start(); } static void WriteText (string text) { Console.WriteLine (text); }
優點是目標方法(這裡是WriteText)可以接收任意數量的參數,並且沒有裝箱操作。不過這需要將一個外部變量放入到匿名方法中,向下面的一樣:
static void Main() { string text = "Before"; Thread t = new Thread (delegate() { WriteText (text); }); text = "After"; t.Start(); } static void WriteText (string text) { Console.WriteLine (text); }
匿名方法出現了一種怪異的現象:當外部變量被後面的代碼修改了值的時候,線程可能會通過外部變量進行無意的互動。換個角度看,有意的互動(通常通過字段)也可以采用這種方式!一旦線程開始運行了,外部變量最好被處理成只讀的——除非有人願意使用適當的鎖。
另一種較常見的方式是將對象實例的方法而不是靜態方法傳入到線程中,對象實例的屬性可以告訴線程要做什麼,如下重寫了上節的例子:
class ThreadDemo { bool upper; static void Main() { ThreadDemo instance1 = new ThreadDemo(); instance1.upper = true; Thread t = new Thread (instance1.Go); t.Start(); ThreadDemo instance2 = new ThreadDemo(); instance2.Go(); // 主線程——運行 upper=false } void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!"); }
命名線程
線程可以通過它的Name屬性進行命名,這非常有利於調試:可以用Console.WriteLine打印出線程的名字,Microsoft Visual Studio可以將線程的名字顯示在調試工具欄的位置上。線程的名字可以在被任何時間設置——但只能設置一次,重命名會引發異常。
程序的主線程也可以被命名,下面例子裡主線程通過CurrentThread命名:
class ThreadNaming { static void Main() { Thread.CurrentThread.Name = "main"; Thread worker = new Thread (Go); worker.Name = "worker"; worker.Start(); Go(); } static void Go() { Console.WriteLine ("Hello from " + Thread.CurrentThread.Name); } }
前台和後台線程
線程默認為前台線程,這意味著任何前台線程在運行都會保持程序存活。C#也支持後台線程,當所有前台線程結束後,它們不維持程序的存活。
改變線程從前台到後台不會以任何方式改變它在CPU協調程序中的優先級和狀態。
線程的IsBackground屬性控制它的前後台狀態,如下實例:
class PriorityTest { static void Main (string[] args) { Thread worker = new Thread (delegate() { Console.ReadLine(); }); if (args.Length > 0) worker.IsBackground = true; worker.Start(); } }
如果程序被調用的時候沒有任何參數,工作線程為前台線程,並且將等待ReadLine語句來等待用戶的觸發回車,這期間,主線程退出,但是程序保持運行,因為一個前台線程仍然活著。 另一方面如果有參數傳入Main(),工作線程被賦值為後台線程,當主線程結束程序立刻退出,終止了ReadLine。後台線程這種終止方式,使任何最後操作都被規避了,這是不太合適的。好的方式是明確等待任何後台工作線程完成後再結束程序,可能用一個timeout(大多用Thread.Join)。如果因為某種原因某個工作線程無法完成,可以試圖終止它,如果失敗了,再拋棄線程,允許它與進程一起消亡。(記錄是一個難題,但在這個場景下是有意義的)
擁有一個後台工作線程是有益的,最直接的理由是結束程序時它可能有最後的發言權,與不會消亡的前台線程一起保證程序的正常退出。拋棄一個前台工作線程風險更大,尤其對Windows Forms程序,因為程序直到主線程結束時才退出(至少對用戶來說),但是它的進程仍然運行著。它將從應用程序欄消失不見,但卻可以在在Windows任務管理器進程欄找到它。除非手動找到並結束它,否則將繼續消耗資源,並可能阻止一個新的實例的重新開始運行或影響它的特性。
對於程序失敗退出的普遍原因就是存在“被忘記”的前台線程。
線程優先級
線程的Priority 屬性確定了線程相對於其它同一進程的活動的線程擁有多少執行時間,以下是級別:
enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
只有多個線程同時為活動時,優先級才有作用。
設置一個線程的優先級為高一些,並不意味著它能執行實時的工作,因為它受限於程序的進程級別。要執行實時的工作,必須提升在System.Diagnostics 命名空間下Process的級別,像下面這樣:
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
ProcessPriorityClass.High 其實是一個時間片中的最高優先級別:Realtime。設置進程級別到Realtime通知操作系統:你不想讓你的進程被搶占了。如果你的程序進入一個偶然的死循環,可以預期,操作系統被鎖住了,除了關機沒有什麼可以拯救你了!基於此,High大體上被認為最高的有用進程級別。
如果一個實時的程序有一個用戶界面,提升進程的級別是不太好的,因為當用戶界面UI過於復雜的時候,界面的更新耗費過多的CPU時間,拖慢了整台電腦。 降低主線程的級別、提升進程的級別、確保實時線程不進行界面刷新,但這樣並不能避免電腦越來越慢,因為操作系統仍會撥出過多的CPU給整個進程。最理想的方案是使實時工作和用戶界面在不同的進程(擁有不同的優先級)運行,通過Remoting或共享內存方式進行通信,共享內存需要Win32 API中的 P/Invoking。(可以搜索看看CreateFileMapping 和 MapViewOfFile)
異常處理
任何線程創建范圍內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) { 記錄異常日志,並且或通知另一個線程 我們發生錯誤 ... }
從.NET 2.0開始,任何線程內的未處理的異常都將導致整個程序關閉,這意味著忽略異常不再是一個選項了。因此為了避免由未處理異常引起的程序崩潰,try/catch塊需要出現在每個線程進入的方法內,至少要在產品程序中應該如此。對於經常使用“全局”異常處理的Windows Forms程序員來說,這可能有點麻煩,像下面這樣:
using System; using System.Threading; using System.Windows.Forms; static class Program { static void Main() { Application.ThreadException += HandleError; Application.Run (new MainForm()); } static void HandleError (object sender, ThreadExceptionEventArgs e) { 記錄異常或者退出程序或者繼續運行... } }
Application.ThreadException事件在異常被拋出時觸發,以一個Windows信息(比如:鍵盤,鼠標活著 "paint" 等信息)的方式,簡言之,一個Windows Forms程序的幾乎所有代碼。雖然這看起來很完美,它使人產生一種虛假的安全感——所有的異常都被中央異常處理捕捉到了。由工作線程拋出的異常便是一個沒有被Application.ThreadException捕捉到的很好的例外。(在Main方法中的代碼,包括構造器的形式,在Windows信息開始前先執行)
.NET framework為全局異常處理提供了一個更低級別的事件:AppDomain.UnhandledException,這個事件在任何類型的程序(有或沒有用戶界面)的任何線程有任何未處理的異常觸發。盡管它提供了好的不得已的異常處理解決機制,但是這不意味著這能保證程序不崩潰,也不意味著能取消.NET異常對話框。
在產品程序中,明確地使用異常處理在所有線程進入的方法中是必要的,可以使用包裝類和幫助類來分解工作來完成任務,比如使用BackgroundWorker類。