原文地址:C#多線程之旅(2)——創建和開始線程
C#多線程之旅目錄:
C#多線程之旅(5)——同步機制介紹
C#多線程之旅(6)——詳解多線程中的鎖
更多文章正在更新中,敬請期待......
C#多線程之旅(2)——創建和開始線程
代碼下載
Thread_博客園_cnblogs_jackson0714.zip
第一篇~第三篇的代碼示例:
源碼地址:https://github.com/Jackson0714/Threads
一、線程的創建和開始
在第一篇的介紹中,線程使用Thread 類的構造函數來創建,通過傳給一個ThreadStart 委托來實現線程在哪里開始執行。下面是ThreadStart的定義:
// Summary: // Represents the method that executes on a System.Threading.Thread. [ComVisible(true)] public delegate void ThreadStart();
調用一個Start方法,然后設置它開始運行。線程會一直運行直到這個方法返回,然后這個線程結束。
下面是一個例子,使用擴展C#語法創建一個ThreadStart委托:2.1_ThreadStart
1 class ThreadTest 2 { 3 static void Main() 4 { 5 Thread t = new Thread(new ThreadStart(Go)); 6 t.Start(); 7 Go(); 8 Console.ReadKey(); 9 } 10 static void Go() 11 { 12 Console.WriteLine("hello!"); 13 } 14 }
在這個例子中,thread t執行Go(),基本上與主線同時程調用Go()方法,結果是打印出兩個時間接近的hello。
一個線程可以被方便的創建通過指定一個方法組,然后由C#推斷出ThreadStart委托:2.2_Thread
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Thread t = new Thread(Go); 6 t.Start(); 7 Go(); 8 Console.ReadKey(); 9 } 10 11 static void Go() 12 { 13 Console.WriteLine("Go"); 14 } 15 }
另外一種更簡單的方式是使用lambda表達式或者匿名方法:2.3_LambaExpression
static void Main(string[] args) { Thread t = new Thread(()=>Console.WriteLine("Go")); t.Start(); Console.ReadKey(); }
二、傳遞數據給一個線程
1.利用Lambda傳遞一個數據
傳遞參數給線程的目標方法的最簡單的方法是執行一個lambda表達式,該表達式調用一個方法並傳遞期望的參數給這個方法。
2.4_PassingDataToAThread
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
new Thread(() => { Console.WriteLine("a"); Console.WriteLine("b"); }).Start();
你也可以簡單的在C# 2.0里面那樣使用匿名方法做同樣的事:
new Thread(delegate() { Console.WriteLine("a"); Console.WriteLine("b"); }).Start();
3.利用Thread.Start傳遞參數
另外一種方式是傳遞一個參數給Thread的Start方法:
2.6_PassingDataToAThread_ThreadStart
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的構造函數是重載的,接受下面兩種中的任意一種委托:
// 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
for(int i =0;i<10;i++) { new Thread(() => Console.Write(i)).Start(); }
這個輸出是不確定的,下面是一種典型的情況:
這里的問題是變量i在for循環執行時指向同一個內存地址。因此,每一個線程調用Console.Write時,i的值有可能在這個線程運行時改變。
解決方案是使用一個臨時變量:
2.8_LambdaExpressionsAndCapturedVariables_Solution
for (int i = 0; i < 10; i++) { int temp = i; new Thread(() => Console.Write(temp)).Start(); }
變量temp在每個循環迭代中位於不同的內存塊。因此每一個線程捕獲到了不同的內存位置,而且沒有問題。我們可以解釋在之前的代碼中的問題:
2.9_PassingData_TemporaryVariable
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中,我們設置了主線程的名字:
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
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。造成應用程序不能正確地退出的一個通常的原因是有激活的前台線程還存活在。
五、線程優先級
一個線程的優先級決定了在操作系統中它可以得到多少相對其他線程的執行時間,下面是線程優先級的等級:
// 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類來提升進程的優先級:
using (Process p = Process.GetCurrentProcess()) { p.PriorityClass = ProcessPriorityClass.High; }
ProcessPriorityClass.High事實上是優先級最高的一檔:實時。設置一個進程優先級到實時狀態將會導致其他線程無法獲得CPU時間片。如果你的應用程序意外地進入一個無限循環的狀態,你甚至會發現操作被鎖住了,只有電源鍵能夠拯救你了。針對這個原因,High通常對於實時應用程序是最好的選擇。
如果你的實時應用程序有一個用戶界面,提高程序的優先級將會使刷新界面占用昂貴的CPU的時間,且會使整個系統變得運行緩慢(尤其是UI很復雜的時候)。降低主線程優先級且提升進程的優先級來確保實時線程不會被界面重繪所搶占,但是不會解決其他進程對CPU訪問缺乏的問題,因為操作系統整體上會一直分配不成比例的資源給進程。一個理想的解決方案是讓實時線程和用戶界面用不同的優先級運行在不同的進程中,通過遠程和內存映射文件來通信。即使提高了進程優先級,在托管環境中處理硬實時系統需求還是對適用性有限制。此外,潛藏的問題會被自動垃圾回收引進,操作系統會遇到新的挑戰,即使是非托管代碼,使用專用硬件或者特殊的實時平台,那將被最好的解決。
六、異常處理
在任何try/catch/finally 語句塊作用域內創建的線程,當這個線程開始時,這個線程和語句塊是沒有關聯的。
思考下面的程序:
參考例子:2.12_ExceptionHandling
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
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)