在上一篇C#多線程之線程池篇2中,我們主要學習了線程池和並行度以及如何實現取消選項的相關知識。在這一篇中,我們主要學習如何使用等待句柄和超時、使用計時器和使用BackgroundWorker組件的相關知識。
五、使用等待句柄和超時
在這一小節中,我們將學習如何在線程池中實現超時和正確地實現等待。具體操作步驟如下:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,編寫代碼如下所示:
1 using System;
2 using System.Threading;
3 using static System.Console;
4 using static System.Threading.Thread;
5
6 namespace Recipe05
7 {
8 class Program
9 {
10 // CancellationTokenSource:通知System.Threading.CancellationToken,告知其應被取消。
11 static void WorkerOperationWait(CancellationTokenSource cts, bool isTimedOut)
12 {
13 if (isTimedOut)
14 {
15 // 傳達取消請求。
16 cts.Cancel();
17 WriteLine("Worker operation timed out and was canceled.");
18 }
19 else
20 {
21 WriteLine("Worker operation succeeded.");
22 }
23 }
24
25 // CancellationToken:傳播有關應取消操作的通知。
26 // ManualResetEvent:通知一個或多個正在等待的線程已發生事件。
27 static void WorkerOperation(CancellationToken token, ManualResetEvent evt)
28 {
29 for (int i = 0; i < 6; i++)
30 {
31 // 獲取是否已請求取消此標記。如果已請求取消此標記,則為 true;否則為 false。
32 if (token.IsCancellationRequested)
33 {
34 return;
35 }
36 Sleep(TimeSpan.FromSeconds(1));
37 }
38 // 將事件狀態設置為終止狀態,允許一個或多個等待線程繼續。
39 evt.Set();
40 }
41
42 static void RunOperations(TimeSpan workerOperationTimeout)
43 {
44 using (var evt = new ManualResetEvent(false))
45 using (var cts = new CancellationTokenSource())
46 {
47 // 注冊一個等待System.Threading.WaitHandle的委托,並指定一個System.TimeSpan值來表示超時時間。
48 // 第一個參數:要注冊的System.Threading.WaitHandle。使用System.Threading.WaitHandle而非 System.Threading.Mutex。
49 // 第二個參數:waitObject參數終止時調用的System.Threading.WaitOrTimerCallback 委托。
50 // 第三個參數:傳遞給委托的對象。
51 // 第四個參數:System.TimeSpan表示的超時時間。如果timeout為0(零),則函數將測試對象的狀態並立即返回。如果timeout為 -1,則函數的超時間隔永遠不過期。
52 // 第五個參數:如果為true,表示在調用了委托後,線程將不再在waitObject參數上等待;如果為false,表示每次完成等待操作後都重置計時器,直到注銷等待。
53 // 返回值:封裝本機句柄的System.Threading.RegisteredWaitHandle。
54 var worker = ThreadPool.RegisterWaitForSingleObject(evt, (state, isTimedOut) => WorkerOperationWait(cts, isTimedOut), null, workerOperationTimeout, true);
55
56 WriteLine("Starting long running operation...");
57 // ThreadPool.QueueUserWorkItem:將方法排入隊列以便執行。此方法在有線程池線程變得可用時執行。
58 // cts.Token:獲取與此System.Threading.CancellationTokenSource關聯的System.Threading.CancellationToken。
59 ThreadPool.QueueUserWorkItem(_ => WorkerOperation(cts.Token, evt));
60
61 Sleep(workerOperationTimeout.Add(TimeSpan.FromSeconds(2)));
62
63 // 取消由System.Threading.ThreadPool.RegisterWaitForSingleObject方法發出的已注冊等待操作。
64 worker.Unregister(evt);
65 }
66 }
67
68 static void Main(string[] args)
69 {
70 // 實現超時
71 RunOperations(TimeSpan.FromSeconds(5));
72 // 實現等待
73 RunOperations(TimeSpan.FromSeconds(7));
74 }
75 }
76 }
3、運行該控制台應用程序,運行效果如下圖所示:

線程池還有另一個有用的方法:ThreadPool.RegisterWaitForSingleObject,該方法允許我們將回調方法放入線程池的隊列中,當所提供的等待句柄發送信號或者超時發生時,該回調方法即被執行。這允許我們對線程池中的操作實現超時。
在第71行代碼處,我們在主線程中調用了“RunOperations”方法,並給它的workerOperationTimeout參數傳遞了數值5,表示超時時間為5秒。
在第54行代碼處,我們調用了ThreadPool的“RegisterWaitForSingleObject”靜態方法,並指定了回調方法所要執行的操作是“WorkerOperationWait”方法,超時時間是5秒。
在第59行代碼處,我們調用ThreadPool的“QueueUserWorkItem”靜態方法來執行“WorkerOperation”方法,而該方法所消耗的時間為6秒,在這六秒中內已經在線程池中發送了超時,所以會執行第13~18行和第32~35行處的代碼。
在第73行代碼處,我們傳遞了數值7給“RunOperations”方法,設置線程池的超時時間為7秒,因為“WorkerOperation”方法的執行時間為6秒,所以在這種情況下沒有發生超時,成功執行完畢“WorkerOperation”方法。
六、使用計時器
在這一小節中,我們將學習如何使用System.Threading.Timer對象在線程池中定期地調用一個異步操作。具體操作步驟如下所示:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,編寫代碼如下所示:
1 using System;
2 using System.Threading;
3 using static System.Console;
4 using static System.Threading.Thread;
5
6 namespace Recipe06
7 {
8 class Program
9 {
10 static Timer timer;
11
12 static void TimerOperation(DateTime start)
13 {
14 TimeSpan elapsed = DateTime.Now - start;
15 WriteLine($"{elapsed.Seconds} seconds from {start}. Timer thread pool thread id: {CurrentThread.ManagedThreadId}");
16 }
17
18 static void Main(string[] args)
19 {
20 WriteLine("Press 'Enter' to stop the timer...");
21 DateTime start = DateTime.Now;
22 // 初始化Timer類的新實例,使用System.TimeSpan值來度量時間間隔。
23 // 第一個參數:一個System.Threading.TimerCallback委托,表示要執行的方法。
24 // 第二個參數:一個包含回調方法要使用的信息的對象,或者為null。
25 // 第三個參數:System.TimeSpan,表示在callback參數調用它的方法之前延遲的時間量。指定-1毫秒以防止啟動計時器。指定零(0)可立即啟動計時器。
26 // 第四個參數:在調用callback所引用的方法之間的時間間隔。指定-1毫秒可以禁用定期終止。
27 timer = new Timer(_ => TimerOperation(start), null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2));
28 try
29 {
30 Sleep(TimeSpan.FromSeconds(6));
31 // 更改計時器的啟動時間和方法調用之間的時間間隔,使用System.TimeSpan值度量時間間隔。
32 // 第一個參數:一個System.TimeSpan,表示在調用構造System.Threading.Timer時指定的回調方法之前的延遲時間量。指定負-1毫秒以防止計時器重新啟動。指定零(0)可立即重新啟動計時器。
33 // 第二個參數:在構造System.Threading.Timer時指定的回調方法調用之間的時間間隔。指定-1毫秒可以禁用定期終止。
34 timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(4));
35 ReadLine();
36 }
37 finally
38 {
39 timer.Dispose();
40 }
41 }
42 }
43 }
3、運行該控制台應用程序,運行效果(每次運行效果可能不同)如下圖所示:

首先,我們創建了一個Timer實例,它的構造方法的第一個參數是一個lambda表達式,表示要在線程池中執行的代碼,在該表達式中我們調用了“TimerOperation”方法,並給它提供了一個開始時間值。由於我們沒有使用state對象,因此我們給Timer的構造方法的第二個參數傳遞了null。第三個參數表示第一次執行“TimerOperation”所要花費的時間為1秒鐘。第四個參數表示每次調用“TimerOperation”之間的時間間隔為2秒鐘。
在主線程阻塞6秒鐘之後,我們調用了Timer實例的“Change”方法,更改了每次調用“TimerOperation”之間的時間間隔為4秒鐘。
最後,我們等待輸入“Enter”鍵來結束應用程序。
七、使用BackgroundWorker組件
在這一小節中,我們學習另外一種異步編程的方式:BackgroundWorker組件。在這個組件的幫助下,我們可以通過一系列事件和事件處理方法組織我們的異步代碼。具體操作步驟如下所示:
1、使用Visual Studio 2015創建一個新的控制台應用程序。
2、雙擊打開“Program.cs”文件,編寫代碼如下所示:
1 using System;
2 using System.ComponentModel;
3 using System.Threading;
4 using static System.Console;
5 using static System.Threading.Thread;
6
7 namespace Recipe07
8 {
9 class Program
10 {
11 static void WorkerDoWork(object sender, DoWorkEventArgs e)
12 {
13 WriteLine($"DoWork thread pool thread id: {CurrentThread.ManagedThreadId}");
14 var bw = (BackgroundWorker)sender;
15 for (int i = 1; i <= 100; i++)
16 {
17 // 獲取一個值,指示應用程序是否已請求取消後台操作。
18 // 如果應用程序已請求取消後台操作,則為 true;否則為 false。默認值為 false。
19 if (bw.CancellationPending)
20 {
21 e.Cancel = true;
22 return;
23 }
24
25 if (i % 10 == 0)
26 {
27 // 引發 System.ComponentModel.BackgroundWorker.ProgressChanged 事件。
28 // 參數:已完成的後台操作所占的百分比,范圍從 0% 到 100%。
29 bw.ReportProgress(i);
30 }
31
32 Sleep(TimeSpan.FromSeconds(0.1));
33 }
34
35 // 獲取或設置表示異步操作結果的值。
36 e.Result = 42;
37 }
38
39 static void WorkerProgressChanged(object sender, ProgressChangedEventArgs e)
40 {
41 // e.ProgressPercentage:獲取異步任務的進度百分比。
42 WriteLine($"{e.ProgressPercentage}% completed. Progress thread pool thread id: {CurrentThread.ManagedThreadId}");
43 }
44
45 static void WorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
46 {
47 WriteLine($"Completed thread pool thread id: {CurrentThread.ManagedThreadId}");
48 // e.Error:獲取一個值,該值指示異步操作期間發生的錯誤。
49 if (e.Error != null)
50 {
51 // 打印出異步操作期間發生的錯誤信息。
52 WriteLine($"Exception {e.Error.Message} has occured.");
53 }
54 else if (e.Cancelled) // 獲取一個值,該值指示異步操作是否已被取消。
55 {
56 WriteLine($"Operation has been canceled.");
57 }
58 else
59 {
60 // e.Result:獲取表示異步操作結果的值。
61 WriteLine($"The answer is: {e.Result}");
62 }
63 }
64
65 static void Main(string[] args)
66 {
67 // 初始化System.ComponentModel.BackgroundWorker類的新實例。該類在單獨的線程上執行操作。
68 var bw = new BackgroundWorker();
69 // 獲取或設置一個值,該值指示System.ComponentModel.BackgroundWorker能否報告進度更新。
70 // 如果System.ComponentModel.BackgroundWorker支持進度更新,則為true;否則為false。默認值為false。
71 bw.WorkerReportsProgress = true;
72 // 獲取或設置一個值,該值指示System.ComponentModel.BackgroundWorker是否支持異步取消。
73 // 如果System.ComponentModel.BackgroundWorker支持取消,則為true;否則為false。默認值為false。
74 bw.WorkerSupportsCancellation = true;
75
76 // 調用System.ComponentModel.BackgroundWorker.RunWorkerAsync時發生。
77 bw.DoWork += WorkerDoWork;
78 // 調用System.ComponentModel.BackgroundWorker.ReportProgress(System.Int32)時發生。
79 bw.ProgressChanged += WorkerProgressChanged;
80 // 當後台操作已完成、被取消或引發異常時發生。
81 bw.RunWorkerCompleted += WorkerCompleted;
82
83 // 開始執行後台操作。
84 bw.RunWorkerAsync();
85
86 WriteLine("Press C to cancel work");
87
88 do
89 {
90 // 獲取用戶按下的下一個字符或功能鍵。按下的鍵可以選擇顯示在控制台窗口中。
91 // 確定是否在控制台窗口中顯示按下的鍵。如果為 true,則不顯示按下的鍵;否則為 false。
92 if (ReadKey(true).KeyChar == 'C')
93 {
94 // 請求取消掛起的後台操作。
95 bw.CancelAsync();
96 }
97 }
98 // 獲取一個值,指示System.ComponentModel.BackgroundWorker是否正在運行異步操作。
99 // 如果System.ComponentModel.BackgroundWorker正在運行異步操作,則為true;否則為false。
100 while (bw.IsBusy);
101 }
102 }
103 }
3、運行該控制台應用程序,運行效果(每次運行效果可能不同)如下圖所示:

在第68行代碼處,我們創建了一個BackgroundWorker組件的實例,並且在第71行代碼和第74行代碼處明確地說明該實例支持進度更新和異步取消操作。
在第77行代碼、第79行代碼和第81行代碼處,我們給該實例掛載了三個事件處理方法。每當DoWork、ProgressChanged和RunWorkerCompleted事件發生時,都會執行相應的“WorkerDoWork方法”、“WorkerProgressChanged”方法和“WorkerCompleted”方法。
其他代碼請參考注釋。
源碼下載