在System.Threading 命名空間下,包含了用於創建和控制線程的Thread 類。對線程的常用操作有:啟動線程、終止線程、合並線程和讓線程休眠等。
1 啟動線程
在使用線程前,首先要創建一個線程。其一般形式為:
Thread t=new Thread(enterPoint);
其中enterPoint 為線程的入口,即線程開始執行的方法。在托管代碼中,通過委托處理線程執行的代碼。例如:
Thread t=new Thread(new ThreadStart(methodName));
創建線程實例后,就可以調用Start 方法啟動線程了。
2 終止線程
線程啟動后,當不需要某個線程繼續執行的時候,有兩種終止線程的方法。
一種是事先設置一個布爾變量,在其他線程中通過修改該變量的值作為傳遞給該線程是否需要終止的判斷條件,而在該線程中循環判斷該條件,以確定是否退出線程,這是結束線程的比較好的方法,實際編程中一般使用這種方法。
第二種方法是通過調用Thread 類的Abort 方法強行終止線程。例如:
t.Abort();
Abort 方法沒有任何參數,線程一旦被終止,就無法再重新啟動。由於Abort 通過拋出異常強行終止結束線程,因此在實際編程中,應該盡量避免采用這種方法。
調用Abort 方法終止線程時,公共語言運行庫(CLR)會引發ThreadAbortException 異常,程序員可以在線程中捕獲ThreadAbortException 異常,然后在異常處理的Catch 塊或者Finally塊中作釋放資源等代碼處理工作;但是,線程中也可以不捕獲ThreadAbortException 異常,而由系統自動進行釋放資源等處理工作。
注意,如果線程中捕獲了ThreadAbortException 異常,系統在finally 子句的結尾處會再次引發ThreadAbortException 異常,如果沒有finally 子句,則會在Catch 子句的結尾處再次引發該異常。為了避免再次引發異常,可以在finally 子句的結尾處或者Catch 子句的結尾處調用System.Threading.Thread.ResetAbort 方法防止系統再次引發該異常。
使用Abort 方法終止線程,調用Abort 方法后,線程不一定會立即結束。這是因為系統在結束線程前要進行代碼清理等工作,這種機制可以使線程的終止比較安全,但清理代碼需要一定的時間,而我們並不知道這個工作將需要多長時間。因此,調用了線程的Abort 方法后,如果系統自動清理代碼的工作沒有結束,可能會出現類似死機一樣的假象。為了解決這個問題,可以在主線程中調用子線程對象的Join 方法,並在Join 方法中指定主線程等待子線程結束的等待時間。
3 合並線程
Join 方法用於把兩個並行執行的線程合並為一個單個的線程。如果一個線程t1 在執行的過程中需要等待另一個線程t2 結束后才能繼續執行,可以在t1 的程序模塊中調用t2 的join()方法。例如:
t2.Join();
這樣t1 在執行到t2.Join()語句后就會處於阻塞狀態,直到t2 結束后才會繼續執行。
但是假如t2 一直不結束,那么等待就沒有意義了。為了解決這個問題,可以在調用t2 的Join 方法的時候指定一個等待時間,這樣t1 這個線程就不會一直等待下去了。例如,如果希望將t2 合並到t1 后,t1 只等待100 毫秒,然后不論t2 是否結束,t1 都繼續執行,就可以在t1中加上語句:
t2.Join(100);
Join 方法通常和Abort 一起使用。
由於調用某個線程的Abort 方法后,我們無法確定系統清理代碼的工作什么時候才能結束,因此如果希望主線程調用了子線程的Abort 方法后,主線程不必一直等待,可以調用子線程的Join 方法將子線程連接到主線程中,並在連接方法中指定一個最大等待時間,這樣就能使主線程繼續執行了。
4 讓線程休眠
在多線程應用程序中,有時候並不希望某一個線程繼續執行,而是希望該線程暫停一段時間,等待其他線程執行之后再繼續執行。這時可以調用Thread 類的Sleep 方法,即讓線程休眠。例如:
Thread.Sleep(1000);
這條語句的功能是讓當前線程休眠1000 毫秒。
注意,調用Sleep 方法的是類本身,而不是類的實例。休眠的是該語句所在的線程,而不是其他線程。
5 線程優先級
當線程之間爭奪CPU 時間片時,CPU 是按照線程的優先級進行服務的。在C#應用程序中,可以對線程設定五個不同的優先級,由高到低分別是Highest、AboveNormal、Normal、BelowNormal 和Lowest。在創建線程時如果不指定其優先級,則系統默認為Normal。假如想讓一些重要的線程優先執行,可以使用下面的方法為其賦予較高的優先級:
Thread t=new Thread(new ThreadStart(enterpoint)); t.priority=ThreadPriority.AboveNormal;
通過設置線程的優先級可以改變線程的執行順序,所設置的優先級僅僅適用於這些線程所屬的進程。
注意,當把某線程的優先級設置為Highest 時,系統上正在運行的其他線程都會終止,所以使用這個優先級別時要特別小心。
6 線程池
線程池是一種多線程處理形式,為了提高系統性能,在許多地方都要用到線程池技術。例如,在一個C/S 模式的應用程序中的服務器端,如果每到一個請求就創建一個新線程,然后在新線程中為其請求服務的話,將不可避免的造成系統開銷的增大。實際上,創建太多的線程可能會導致由於過度使用系統資源而耗盡內存。為了防止資源不足,服務器端應用程序應采取一定的辦法來限制同一時刻處理的線程數目。
線程池為線程生命周期的開銷問題和資源不足問題提供了很好的解決方案。通過對多個任務重用線程,線程創建的開銷被分攤到了多個任務上。其好處是,由於請求到達時線程已經存在,所以無意中也就消除了線程創建所帶來的延遲。這樣,就可以立即為新線程請求服務,使其應用程序響應更快。而且,通過適當地調整線程池中的線程數目,也就是當請求的數目超過了規定的最大數目時,就強制其他任何新到的請求一直等待,直到獲得一個線程來處理為止,從而可以防止資源不足。
線程池適用於需要多個線程而實際執行時間又不多的場合,比如有些常處於阻塞狀態的線程。當一個應用程序服務器接受大量短小線程的請求時,使用線程池技術是非常合適的,它可以大大減少線程創建和銷毀的次數,從而提高服務器的工作效率。但是如果線程要求運行的時間比較長的話,那么此時線程的運行時間比線程的創建時間要長得多,僅靠減少線程的創建時間對系統效率的提高就不是那么明顯了,此時就不適合使用線程池技術,而需要借助其他的技術來提高服務器的服務效率。
7 同步
同步是多線程中一個非常重要的概念。所謂同步,是指多個線程之間存在先后執行順序的關聯關系。如果一個線程必須在另一個線程完成某個工作后才能繼續執行,則必須考慮如何讓
其保持同步,以確保在系統上同時運行多個線程而不會出現邏輯錯誤。
當兩個線程t1 和t2 有相同的優先級,並且同時在系統上運行時,如果先把時間片分給t1使用,它在變量variable1 中寫入某個值,但如果在時間片用完時它仍沒有完成寫入,這時由於時間片已經分給t2 使用,而t2 又恰好要嘗試讀取該變量,它可能就會讀出錯誤的值。這時,如果使用同步僅允許一個線程使用variable1,在該線程完成對variable1 的寫入工作后再讓t2讀取這個值,就可以避免出現此類錯誤。
為了對線程中的同步對象進行操作,C#提供了lock 語句鎖定需要同步的對象。lock 關鍵字確保當一個線程位於代碼的臨界區時,另一個線程不進入臨界區。如果其他線程試圖進入鎖定的代碼,則它將一直等待(即被阻塞),直到該對象被釋放。比如線程t1 對variable1 操作時,為了避免其他線程也對其進行操作,可以使用lock 語句鎖定variable1,實現代碼為:
lock(variable1) { variable1++; }
注意,鎖定的對象一定要聲明為private,不要鎖定public 類型的對象,否則將會使lock語句無法控制,從而引發一系列問題。
另外還要注意,由於鎖定一個對象之后,其他任何線程都不能訪問這個對象,需要使用該對象的線程就只能等待該對象被解除鎖定后才能使用。因此如果在鎖定和解鎖期間處理的對象過多,就會降低應用程序的性能。
還有,如果兩個不同的線程同時鎖定兩個不同的變量,而每個線程又都希望在鎖定期間訪問對方鎖定的變量,那么兩個線程在得到對方變量的訪問權之前都不會釋放自己鎖定的對象,從而產生死鎖。在編寫程序時,要注意避免這類操作引起的問題。
【例】線程的基本用法
(1) 新建一個名為ThreadExample 的Windows 應用程序,界面設計如圖所示。
(2) 向設計窗體拖放一個Timer 組件,不改變自動生成的對象名。
(3) 添加命名空間引用:
using System.Threading;
(4) 在構造函數上方添加字段聲明:
StringBuilder sb = new StringBuilder(); Thread thread1; Thread thread2;
(5) 直接添加代碼:
private void AppendString(string s) { lock (sb) { sb.Append(s); } } public void Method1() { while (true) { Thread.Sleep(100); //線程休眠100 毫秒 AppendString("a"); } } public void Method2() { while (true) { Thread.Sleep(100); //線程休眠100 毫秒 AppendString("b"); } }
(6) 分別在【啟動線程】和【終止線程】按鈕的Click 事件中添加代碼:
private void buttonStart_Click(object sender, EventArgs e) { sb.Remove(0, sb.Length); timer1.Enabled = true; thread1 = new Thread(new ThreadStart(Method1)); thread2 = new Thread(new ThreadStart(Method2)); thread1.Start(); thread2.Start(); } private void buttonAbort_Click(object sender, EventArgs e) { thread1.Abort(); thread1.Join(10); thread2.Abort(); thread2.Join(10); }
(7) 在timer1 的Tick 事件中添加代碼:
private void timer1_Tick(object sender, EventArgs e) { if (thread1.IsAlive == true || thread2.IsAlive == true) { richTextBox1.Text = sb.ToString(); } else { timer1.Enabled = false; } }
(8) 按<F5>鍵編譯並執行,單擊【啟動線程】后,再單擊【終止線程】,從運行結果中可以看到,兩個具有相同優先級的線程同時執行時,在richTextBox1 中添加的字符個數基本上相同。