在上一篇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”方法。
其他代碼請參考注釋。
源碼下載