2、優雅的退出線程
上一節我們講了如何建立和啓動線程,那麼線程應該如何退出呢?
按照要求,無論使用何種編程語言,線程都必須自然退出,而不應該被迫退出。所謂自然退出,就是線程的入口方法執行完畢退出(包括使用異常跳出方法、使用return跳出方法或令方法運行完畢),線程入口方法執行完畢,標誌着線程退出,此時.net Framework會繼續執行一段代碼,回收線程佔用的資源。
實際上,操作系統提供了方法去殺死一個線程,在Win32編程中,使用函數TerminateThread可以殺死一個線程,但這種方法並不推薦(參見《Windows核心編程》),因爲這樣去殺死一個線程,一方面有可能導致線程佔用的資源得不到釋放,另一方面,當線程被殺死的時候,我們無法得知線程正在做什麼,很有可能它正在執行非常重要的代碼(例如正在向磁盤寫數據),突然停止線程運行會導致很多無法預料的問題。
在.net Framework中沒有提供任何可以殺死線程的方法,所以我們只能等待線程執行完畢後自然死亡。
對於線程中沒有無限循環(包括循環次數非常多的循環)或者阻塞操作(線程同步中詳細講解),即可以在有限時間內執行完畢的線程,編程者無需干預其退出,線程會自己執行完畢後退出;
對於線程中有無限循環(包括循環次數很多的循環)或者阻塞操作,即如果不加干預線程無法結束運行或經過很久才能結束運行,編程者需要干預其退出,通過編程令線程能夠執行完畢後退出。
上一節我們使用了一種通過Thread對象的Abort方法在一個線程停止另一個線程的運行方式,但這種方法頗爲暴力,具體表現爲:
通過異常來處理程序的分支流程是不推薦的,應該使用if, swtich, for, while等傳統語句;
一旦調用了Thread對象的Abort方法,線程立即拋出ThreadAbortException異常並通過catch塊退出,此時無法得知線程代碼正運行在哪一行,是否可以退出。如果線程裏還有處理I/O的代碼,則流是否被刷新,是否被關閉,是否寫入成功等等都無法控制。
我們需要一種方法,在特定的某一行安全的代碼上退出線程,從而不會引發上述的問題。
編程的方法很簡單,設置一個可以在多個線程方法中都可以訪問到的布爾值,線程方法中使用代碼檢測到這個布爾值變化後退出方法即可。
在上一節使用的代碼中,做如下改動:
FormMain.cs
1 using System;
2 using System.Threading;
3 using System.Windows.Forms;
4
5 namespace Edu.Study.Multithreading.HappyEnd {
6
7 /// <summary>
8 /// 線程中操作文本框的委託類型
9 /// </summary>
10 /// <param name="textBox">要操作的文本框</param>
11 /// <param name="num">文本框中顯示的數字</param>
12 public delegate void SetTextBoxHandler(TextBox textBox, int num);
13
14 /// <summary>
15 /// 關閉窗口的委託類型
16 /// </summary>
17 public delegate void CloseFormHandler();
18
19
20 /// <summary>
21 /// 主窗體類
22 /// </summary>
23 public partial class FormMain : Form {
24
25 private Thread thread1, thread2;
26
27 // 用於標誌線程是否繼續結束的布爾值
28 private bool canThreadFinish = false;
29
30 /// <summary>
31 /// 構造器
32 /// </summary>
33 public FormMain() {
34 InitializeComponent();
35
36 this.thread1 = new Thread(new ParameterizedThreadStart(this.ThreadWork));
37 this.thread2 = new Thread(new ParameterizedThreadStart(this.ThreadWork));
38 }
39
40 /// <summary>
41 /// 線程工作方法(線程入口點方法)
42 /// </summary>
43 /// <param name="arg">線程參數</param>
44 public void ThreadWork(object arg) {
45 int num = 1;
46 while (this.canThreadFinish == false) {
47 this.Invoke(new SetTextBoxHandler(this.SetTextBox), arg, num++);
48 Thread.Sleep(10);
49 }
50 }
51
52 /// <summary>
53 /// 在線程中訪問窗體或控件的委託方法, 符合委託類型SetTextBoxHandler
54 /// </summary>
55 /// <param name="textBox">要操作的文本框</param>
56 /// <param name="num">文本框中顯示的數字</param>
57 private void SetTextBox(TextBox textBox, int num) {
58 textBox.Text = num.ToString();
59 }
60
61 /// <summary>
62 /// 等待線程結束的線程入口方法
63 /// </summary>
64 private void WaitThreadIsClosed(object arg) {
65 // ThreadState枚舉表示線程的當前狀態, Running表示線程正在運行
66 if (this.thread1.ThreadState == ThreadState.Running) {
67 // 先嚐試等待1000毫秒, 看線程是否可以自然結束
68 if (this.thread1.Join(1000) == false) {
69 // 如果等待失敗, 則強行退出線程
70 this.thread1.Abort();
71 }
72 }
73 if (this.thread2.ThreadState == ThreadState.Running) {
74 if (this.thread2.Join(1000) == false) {
75 this.thread2.Abort();
76 }
77 }
78 // 通過委託方法關閉窗體
79 this.Invoke(new CloseFormHandler(this.CloseForm));
80 }
81
82 /// <summary>
83 /// 關閉窗體的委託方法
84 /// </summary>
85 private void CloseForm() {
86 this.Close();
87 }
88
89 /// <summary>
90 /// 窗體關閉事件
91 /// </summary>
92 private void FormMain_FormClosing(object sender, FormClosingEventArgs e) {
93 if (this.canThreadFinish == false) {
94 // 修改標誌變量, 令線程代碼可以退出
95 this.canThreadFinish = true;
96
97 // 啓動線程退出等待線程
98 new Thread(new ParameterizedThreadStart(this.WaitThreadIsClosed)).Start();
99
100 // 取消關閉窗體操作
101 e.Cancel = true;
102 }
103 }
104
105 /// <summary>
106 /// 線程啓動按鈕被點擊事件
107 /// </summary>
108 private void startButton_Click(object sender, EventArgs e) {
109 this.thread1.Start(this.firstTextBox);
110 this.thread2.Start(this.secondTextBox);
111 this.startButton.Enabled = false;
112 }
113 }
114 }
以上是修改後的代碼,新的代碼執行起來和上一節的代碼執行結果相同,但當關閉窗口時,上一節代碼中的兩個線程立即結束,而本節介紹的代碼則必然在第46行結束。也就是說如果用戶關閉窗口,則canThreadFinish標誌字段會被設置爲true(第94行),線程中的循環運行條件不再滿足(第46行)從而導致線程方法退出,但線程必須運行46行才能檢測canThreadFinish字段的值,也就是說在線程方法退出時,編程人員可以確切的知道哪些代碼被執行了,哪些代碼不再執行;
編程人員也可以合理的安排線程退出前要做什麼事情,例如將線程入口方法改爲如下代碼:
public void ThreadWork(object arg) {
int num = 1;
while (true) {
if (this.canThreadFinish == true) {
MessageBox.Show("注意,我要退出啦!");
// 其它處理線程退出前工作的代碼
break;
}
this.Invoke(new SetTextBoxHandler(this.SetTextBox), arg, num++);
Thread.Sleep(10);
}
}
這就叫做優雅的退出線程。
對於在線程中使用Control類的Invoke方法通過委訪問行窗體或控件方法或屬性的情況,還有幾點注意事項:
窗體不能先於線程銷燬,因爲線程總是在特定語句(例如第46行)退出,所以改變標誌變量(canThreadFinish)後,線程並不是立即退出,有可能還會運行一些代碼後才能執行到特定語句退出線程。當線程代碼運行到Invoke方法時,如果窗體已經被關閉,則會引發異常。此時,窗體必須等待並確保線程確實退出了才能關閉;
不能在創建窗體的線程(一般爲主線程)中等待輔助線程結束(調用線程對象的Join方法),因爲調用了Join方法的線程會被阻塞(即不再向下運行,而在Join方法上等待),而Invoke方法會向窗體所在線程的消息循環發送執行委託的消息,窗體線程被阻塞後消息循環也會被阻塞,Invoke方法自然也會被阻塞,結果就是窗體線程和輔助線程均被阻塞,程序進入死鎖;
解決上述問題的方法很簡單:在窗體關閉事件中取消窗體關閉(第101行)並啓動一個輔助線程(第98行)來等候其它輔助線程結束(第64-80行),並在等候成功後由該線程通過委託方法(第85-87行)來關閉窗體(第79行)。用輔助線程來等待其它輔助線程結束,則不會阻塞窗體線程,等待成功後關閉窗體,則可以保證窗體在這些線程退出後才安全關閉。
本節代碼下載
3、前臺線程和後臺線程
如果不做特殊說明,Thread類默認爲前臺線程。
所謂前臺線程,即該線程會阻止進程結束。即便主線程結束了,只要進程中還有一個前臺線程尚未結束,則進程不會結束,直到所有的前臺線程都結束。這是最安全的一種方式,保證線程在結束前做完其所有工作。
如果一個線程的工作不涉及I/O操作(例如進行數據計算,異步更新視圖等),則可以設置其爲後臺線程,這樣的線程無需關心其如何退出,只要主線程退出,這些線程就會被強制結束。
設置線程對象的IsBackground屬性可以將其設置爲後臺線程或從後臺線程設置爲前臺線程,例如:
Thread thread = new Thread(/*某入口方法委託*/);
// 設置線程爲後臺線程
thread.IsBackground = true;
thread.Start(/*入口方法參數*/);
將IsBackground屬性設置爲false,此時只要主線程退出,這個輔助線程就會自行終止。
將IsBackground屬性設置爲true,線程就又會成爲前臺線程。
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/mousebaby808/archive/2010/04/11/5471699.aspx