C#多執行緒之旅目錄:
更多文章正在更新中,敬請期待……
C#多執行緒之旅(2)——建立和開始執行緒
程式碼下載
Thread_部落格園_cnblogs_jackson0714.zip
第一篇~第三篇的程式碼示例:
原始碼地址:https://github.com/Jackson0714/Threads
一、執行緒的建立和開始
在第一篇的介紹中,執行緒使用Thread 類的建構函式來建立,通過傳給一個ThreadStart 委託來實現執行緒在哪裡開始執行。下面是ThreadStart的定義:
1 2 3 4 |
// Summary: // Represents the method that executes on a System.Threading.Thread. [ComVisible(true)] public delegate void ThreadStart(); |
呼叫一個Start方法,然後設定它開始執行。執行緒會一直執行直到這個方法返回,然後這個執行緒結束。
下面是一個例子,使用擴充套件C#語法建立一個ThreadStart委託:2.1_ThreadStart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class ThreadTest { static void Main() { Thread t = new Thread(new ThreadStart(Go)); t.Start(); Go(); Console.ReadKey(); } static void Go() { Console.WriteLine("hello!"); } } |
在這個例子中,thread t執行Go(),基本上與主線同時程呼叫Go()方法,結果是列印出兩個時間接近的hello。
一個執行緒可以被方便的建立通過指定一個方法組,然後由C#推斷出ThreadStart委託:2.2_Thread
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Program { static void Main(string[] args) { Thread t = new Thread(Go); t.Start(); Go(); Console.ReadKey(); } static void Go() { Console.WriteLine("Go"); } } |
另外一種更簡單的方式是使用lambda表示式或者匿名方法:2.3_LambaExpression
1 2 3 4 5 6 |
static void Main(string[] args) { Thread t = new Thread(()=>Console.WriteLine("Go")); t.Start(); Console.ReadKey(); } |
二、傳遞資料給一個執行緒
1.利用Lambda傳遞一個資料
傳遞引數給執行緒的目標方法的最簡單的方法是執行一個lambda表示式,該表示式呼叫一個方法並傳遞期望的引數給這個方法。
2.4_PassingDataToAThread
1 2 3 4 5 6 7 8 9 10 11 |
static void Main(string[] args) { Thread t = new Thread(() => Print("A")); t.Start(); Console.ReadKey(); } static void Print(string message) { Console.WriteLine(message); } |
2.傳遞多個引數
通過這種方式,你可以傳遞任意數量的引數給這個方法。你甚至可以將整個實現包裝在一個多語句的lambda中:
2.5_PassingDataToAThread
1 2 3 4 5 |
new Thread(() => { Console.WriteLine("a"); Console.WriteLine("b"); }).Start(); |
你也可以簡單的在C# 2.0裡面那樣使用匿名方法做同樣的事:
1 2 3 4 5 |
new Thread(delegate() { Console.WriteLine("a"); Console.WriteLine("b"); }).Start(); |
3.利用Thread.Start傳遞引數
另外一種方式是傳遞一個引數給Thread的Start方法:
2.6_PassingDataToAThread_ThreadStart
1 2 3 4 5 6 7 8 9 10 11 |
static void Main(string[] args) { Thread t = new Thread(Print); t.Start("A"); Console.ReadKey(); } static void Print(object messageObj) { string message = (string)messageObj;//必須進行轉換 Console.WriteLine(message); } |
這種方式能夠工作是因為Thread的建構函式是過載的,接受下面兩種中的任意一種委託:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Summary: // Represents the method that executes on a System.Threading.Thread. [ComVisible(true)] public delegate void ThreadStart(); // Summary: // Represents the method that executes on a System.Threading.Thread. // // Parameters: // obj: // An object that contains data for the thread procedure. [ComVisible(false)] public delegate void ParameterizedThreadStart(object obj); |
這個ParameterizedThreadStart的只允許接收一個引數。而且因為它的型別是object,所以通常需要轉換。
4.Lambda表示式和捕獲變數
由我們上面看到的例子可以知道,一個lambda式在傳遞資料給執行緒是最用的。然而,你必須非常小心在開始執行緒後意外修改捕獲變數,因為這些變數是共享的。比如下面的:
2.7_LbdaExpressionsAndCapturedVariables
1 2 3 4 |
for(int i =0;i10;i++) { new Thread(() => Console.Write(i)).Start(); } |
這個輸出是不確定的,下面是一種典型的情況:
這裡的問題是變數i在for迴圈執行時指向同一個記憶體地址。因此,每一個執行緒呼叫Console.Write時,i的值有可能在這個執行緒執行時改變。
解決方案是使用一個臨時變數:
2.8_LambdaExpressionsAndCapturedVariables_Solution
1 2 3 4 5 |
for (int i = 0; i 10; i++) { int temp = i; new Thread(() => Console.Write(temp)).Start(); } |
變數temp在每個迴圈迭代中位於不同的記憶體塊。因此每一個執行緒捕獲到了不同的記憶體位置,而且沒有問題。我們可以解釋在之前的程式碼中的問題:
2.9_PassingData_TemporaryVariable
1 2 3 4 5 6 7 8 |
string text = "A"; Thread a = new Thread(() => Console.WriteLine(text)); text = "B"; Thread b = new Thread(() => Console.WriteLine(text)); a.Start(); b.Start(); |
因為兩個lambda表示式捕獲同樣的text的值,所以B被列印出兩次。
三、命名執行緒
每一個執行緒有一個Name屬性你可以方便用來debugging.當執行緒顯示在Visual Statudio裡面的Threads Window和Debug Loaction toolbar的時候,執行緒的Name屬性是特別有用的。你可以只設定執行緒的名字一次;之後嘗試改變它將會丟擲異常資訊。
靜態的Thread.CurrentThread屬性代表當前執行的執行緒。
在下面的例子2.10_NamingThread中,我們設定了主執行緒的名字:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static void Main(string[] args) { Thread.CurrentThread.Name = "Main Thread"; Thread t = new Thread(Go); t.Name = "Worker Thread"; t.Start(); Go(); Console.ReadKey(); } static void Go() { Console.WriteLine("Go! The current thread is {0}", Thread.CurrentThread.Name); } |
四、前臺執行緒和後臺執行緒
預設情況下,你自己顯示建立的執行緒是前臺執行緒。前臺執行緒保持這個應用程式一直存活只要其中任意一個正在執行,而後臺執行緒不是這樣的。一旦所有的前臺執行緒完成,這個應用程式就結束了, 任何正在執行的後臺執行緒立刻終止。
一個執行緒前臺/後臺的狀態跟它的優先順序和配置的執行時間沒有關聯。
你可以使用執行緒的IsBackgroud屬性查詢或改變一個執行緒的後臺狀態。
下面是例子:2.11_PriorityTest
1 2 3 4 5 6 7 8 9 10 11 12 |
static void Main(string[] args) { Thread t = new Thread(() => Console.ReadKey()); if (args.Length > 0)//如果Main方法沒有傳入引數 { //設定執行緒為後臺執行緒,等待使用者輸入。 //因為主執行緒在t.Start()執行之後就會終止, //所以後臺執行緒t會在主執行緒退出之後,立即終止,應用程式就會結束。 t.IsBackground = true; } t.Start(); } |
如果程式呼叫的時候傳入了引數,則建立的執行緒為前臺執行緒,然後等待使用者輸入。同時,如果主執行緒退出,應用程式將不會退出,因為前臺執行緒t沒有退出。另一方面,如果main方法傳入了引數,則建立的執行緒設定為後臺執行緒。當主執行緒退出時,應用程式立即退出。
當一個程式以這種方式終止,則任何後臺執行緒執行棧裡面的finally 語句塊將會被規避。如果你的執行緒使用finally(or using)語句塊去執行如釋放資源或者刪除臨時檔案的清理工作,這將是一個問題。為了避免這個,你可以顯示地等待後臺執行緒退出應用程式。
這裡有兩種實現方式:
- 如果你自己建立了這個執行緒,可以在這個執行緒上呼叫Join方法。
- 如果你使用執行緒池,可以使用一個事件去等待處理這個執行緒。
在這兩種情況下,你需要指定一個timeout,因此可以結束一個由於某些原因拒絕完成的執行緒。這是你的備選退出策略:在最後,你想要你的應用程式關閉,不需要使用者從工作管理員中刪除。
如果使用者使用工作管理員強制結束一個.NET程式,所有的執行緒像是後臺執行緒一樣終止。這個是觀察到的行為,所以會因為CLR和作業系統的版本而不同。
前臺執行緒不需要這樣對待,但是你必須小心避免可能造成執行緒不能結束的bugs。造成應用程式不能正確地退出的一個通常的原因是有啟用的前臺執行緒還存活在。
五、執行緒優先順序
一個執行緒的優先順序決定了在作業系統中它可以得到多少相對其他執行緒的執行時間,下面是執行緒優先順序的等級:
1 2 3 4 5 6 7 8 9 10 11 12 |
// Summary: // Specifies the scheduling priority of a System.Threading.Thread. [Serializable] [ComVisible(true)] public enum ThreadPriority { Lowest = 0, BelowNormal = 1, Normal = 2, AboveNormal = 3, Highest = 4, } |
當多執行緒同時是啟用的,執行緒優先順序是很重要的。
注意:提高執行緒優先順序時,需要非常小心,這將可能導致其他執行緒對資源訪問的飢餓狀態的問題。
當提升一個執行緒的優先順序時,不會使它執行實時工作,因為它被應用程式的程式優先順序限制了。為了執行實時工作,你也必須通過使用System.Diagnostices的Process類來提升程式的優先順序:
1 2 3 4 |
using (Process p = Process.GetCurrentProcess()) { p.PriorityClass = ProcessPriorityClass.High; } |
ProcessPriorityClass.High事實上是優先順序最高的一檔:實時。設定一個程式優先順序到實時狀態將會導致其他執行緒無法獲得CPU時間片。如果你的應用程式意外地進入一個無限迴圈的狀態,你甚至會發現操作被鎖住了,只有電源鍵能夠拯救你了。針對這個原因,High通常對於實時應用程式是最好的選擇。
如果你的實時應用程式有一個使用者介面,提高程式的優先順序將會使重新整理介面佔用昂貴的CPU的時間,且會使整個系統變得執行緩慢(尤其是UI很複雜的時候)。降低主執行緒優先順序且提升程式的優先順序來確保實時執行緒不會被介面重繪所搶佔,但是不會解決其他程式對CPU訪問缺乏的問題,因為作業系統整體上會一直分配不成比例的資源給程式。
一個理想的解決方案是讓實時執行緒和使用者介面用不同的優先順序執行在不同的程式中,通過遠端和記憶體對映檔案來通訊。即使提高了程式優先順序,在託管環境中處理硬實時系統需求還是對適用性有限制。此外,潛藏的問題會被自動垃圾回收引進,作業系統會遇到新的挑戰,即使是非託管程式碼,使用專用硬體或者特殊的實時平臺,那將被最好的解決。
六、異常處理
在任何try/catch/finally 語句塊作用域內建立的執行緒,當這個執行緒開始時,這個執行緒和語句塊是沒有關聯的。
思考下面的程式:
參考例子:2.12_ExceptionHandling
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static void Main(string[] args) { try { new Thread(Go).Start(); } catch(Exception ex) { Console.WriteLine("Exception"); } Console.ReadKey(); } static void Go() { throw null; } |
try/catch 宣告在這個例子中是無效的,而且新建立的執行緒將會被一個未處理的NullReferenceException所阻斷。當你考慮每一個執行緒有一個單獨的執行路徑這種行為是說得通的。
改進方法是將exception handler移到Go()的方法中:
參考例子:2.13_ExceptionHandling_Remedy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Program { static void Main(string[] args) { new Thread(Go).Start(); Console.ReadKey(); } static void Go() { try { throw null; } catch (Exception ex) { Console.WriteLine(ex.Message); } } } |
你需要在應用程式中的所有執行緒入口方法中新增一個exception handler ,就像你在主執行緒中做的那樣。一個未處理的執行緒會造成整個應用程式關閉,而且會彈出一個不好看的視窗。
在寫這個exception handling 語句塊時,你可能極少忽略這個問題,典型情況是,你可能會記錄exception的詳細資訊,然後可能顯示一個視窗讓使用者去自動去提交這些資訊到你的web server上。然後你可能會關掉這個應用程式–因為這個error毀壞了程式的狀態。然後,這樣做的開銷是使用者可能會丟失他最近的工作,比如開啟的文件。
對於WPF和WinForm應用程式來說,全域性的exception handling 事件(Application.DispatcherUnhandlerException 和Application.ThreadException)只會檢測到主UI執行緒上的丟擲的異常。你還是必須手動處理執行緒的異常。
AppDomain.CurrentDomain.UnhandledException可以檢測任何未處理的異常,但是無法阻止應用程式之後關閉。
然而,某些情形下你不需要線上程上處理異常,因為.NET Framework為你做了這個。下面是沒有提及的內容:
Asynchronous delegates
BackgroudWorker
The Task Parallel Library(conditions apply)