C#語言 線程基礎(2)

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章