這是繼《開始接觸 async/await 異步編程》、《走進異步編程的世界 - 剖析異步方法》後的第三篇。主要介紹在 WinForm 中如何執行異步操作。
下面通過窗體示例演示以下操作-點擊按鈕後:
①將按鈕禁用,並將標簽內容改成:“Doing”(表示執行中);
②線程掛起3秒(模擬耗時操作);
③啟用按鈕,將標簽內容改為:“Complete”(表示執行完成)。
1 public partial class Form1 : Form 2 { 3 public Form1() 4 { 5 InitializeComponent(); 6 } 7 8 private void btnDo_Click(object sender, EventArgs e) 9 { 10 btnDo.Enabled = false; 11 lblText.Text = @"Doing"; 12 13 Thread.Sleep(3000); 14 15 btnDo.Enabled = true; 16 lblText.Text = @"Complete"; 17 } 18 }
可是執行結果卻是:
圖1-1
【發現的問題】
①好像沒有變成“Doing”?
②並且拖動窗口的時候卡住不動了?
③3秒後突然變到想拖動到的位置?
④同時文本變成“Complete”?
【分析】GUI 程序在設計中要求所有的顯示變化都必須在主 GUI 線程中完成,如點擊事件和移動窗體。Windows 程序時通過 消息來實現,消息放入消息泵管理的消息隊列中。點擊按鈕時,按鈕的Click消息放入消息隊列。消息泵從隊列中移除該消息,並開始處理點擊事件的代碼,即 btnDo_Click 事件的代碼。
btnDo_Click 事件會將觸發行為的消息放入隊列,但在 btnDo_Click 時間處理程序完全退出前(線程掛起 3 秒退出前),消息都無法執行。(3 秒後)接著所有行為都發生了,但速度太快肉眼無法分辨才沒有發現標簽改成“Doing”。
圖1-2 點擊事件
圖1-3 點擊事件具體執行過程
現在我們加入 async/await 特性。
1 public partial class Form1 : Form 2 { 3 public Form1() 4 { 5 InitializeComponent(); 6 } 7 8 private async void btnDo_Click(object sender, EventArgs e) 9 { 10 btnDo.Enabled = false; 11 lblText.Text = @"Doing"; 12 13 await Task.Delay(3000); 14 15 btnDo.Enabled = true; 16 lblText.Text = @"Complete"; 17 } 18 }
圖1-4
現在,就是原先希望看到的效果。
【分析】btnDo_Click 事件處理程序先將前兩條消息壓入隊列,然後將自己從處理器移出,在3秒後(等待空閒任務完成後 Task.Delay )再將自己壓入隊列。這樣可以保持響應,並保證所有的消息可以在線程掛起的時間內被處理。
Task.Yield 方法創建一個立刻返回的 awaitable。等待一個Yield可以讓異步方法在執行後續部分的同時返回到調用方法。可以將其理解為 離開當前消息隊列,回到隊列末尾,讓 CPU 有時間處理其它任務。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 const int num = 1000000; 6 var t = DoStuff.Yield1000(num); 7 8 Loop(num / 10); 9 Loop(num / 10); 10 Loop(num / 10); 11 12 Console.WriteLine($"Sum: {t.Result}"); 13 14 Console.Read(); 15 } 16 17 /// <summary> 18 /// 循環 19 /// </summary> 20 /// <param name="num"></param> 21 private static void Loop(int num) 22 { 23 for (var i = 0; i < num; i++) ; 24 } 25 } 26 27 internal static class DoStuff 28 { 29 public static async Task<int> Yield1000(int n) 30 { 31 var sum = 0; 32 for (int i = 0; i < n; i++) 33 { 34 sum += i; 35 if (i % 1000 == 0) 36 { 37 await Task.Yield(); //創建異步產生當前上下文的等待任務 38 } 39 } 40 41 return sum; 42 } 43 }
圖1.1-1
上述代碼每執行1000次循環就調用 Task.Yield 方法創建一個等待任務,讓處理器有時間處理其它任務。該方法在 GUI 程序中是比較有用的。
將剛才的窗口程序的點擊事件稍微改動一下。
1 public partial class Form1 : Form 2 { 3 public Form1() 4 { 5 InitializeComponent(); 6 7 //async (sender, e) 異步表達式 8 btnDo.Click += async (sender, e) => 9 { 10 Do(false, "Doing"); 11 12 await Task.Delay(3000); 13 14 Do(true, "Finished"); 15 }; 16 } 17 18 private void Do(bool isEnable, string text) 19 { 20 btnDo.Enabled = isEnable; 21 lblText.Text = text; 22 } 23 }
還是原來的配方,還是熟悉的味道,還是原來哪個窗口,變的只是內涵。
圖2-1
現在在原來的基礎上添加了進度條,以及取消按鈕。
1 public partial class Form1 : Form 2 { 3 private CancellationTokenSource _source; 4 private CancellationToken _token; 5 6 public Form1() 7 { 8 InitializeComponent(); 9 } 10 11 /// <summary> 12 /// Do 按鈕事件 13 /// </summary> 14 /// <param name="sender"></param> 15 /// <param name="e"></param> 16 private async void btnDo_Click(object sender, EventArgs e) 17 { 18 btnDo.Enabled = false; 19 20 _source = new CancellationTokenSource(); 21 _token = _source.Token; 22 23 var completedPercent = 0; //完成百分比 24 const int time = 10; //循環次數 25 const int timePercent = 100 / time; //進度條每次增加的進度值 26 27 for (var i = 0; i < time; i++) 28 { 29 if (_token.IsCancellationRequested) 30 { 31 break; 32 } 33 34 try 35 { 36 await Task.Delay(500, _token); 37 completedPercent = (i + 1) * timePercent; 38 } 39 catch (Exception) 40 { 41 completedPercent = i * timePercent; 42 } 43 finally 44 { 45 progressBar.Value = completedPercent; 46 } 47 } 48 49 var msg = _token.IsCancellationRequested ? $"進度為:{completedPercent}% 已被取消!" : $"已經完成"; 50 51 MessageBox.Show(msg, @"信息"); 52 53 progressBar.Value = 0; 54 InitTool(); 55 } 56 57 /// <summary> 58 /// 初始化窗體的工具控件 59 /// </summary> 60 private void InitTool() 61 { 62 progressBar.Value = 0; 63 btnDo.Enabled = true; 64 btnCancel.Enabled = true; 65 } 66 67 /// <summary> 68 /// 取消事件 69 /// </summary> 70 /// <param name="sender"></param> 71 /// <param name="e"></param> 72 private void btnCancel_Click(object sender, EventArgs e) 73 { 74 if (btnDo.Enabled) return; 75 76 btnCancel.Enabled = false; 77 _source.Cancel(); 78 } 79 }
圖3-1
與 async/await 不同的是,你有時候可能需要一個額外的線程,在後台持續完成某項任務,並不時與主線程通信,這時就需要用到 BackgroundWorker 類。主要用於 GUI 程序。
書中的千言萬語不及一個簡單的示例。
1 public partial class Form2 : Form 2 { 3 private readonly BackgroundWorker _worker = new BackgroundWorker(); 4 5 public Form2() 6 { 7 InitializeComponent(); 8 9 //設置 BackgroundWorker 屬性 10 _worker.WorkerReportsProgress = true; //能否報告進度更新 11 _worker.WorkerSupportsCancellation = true; //是否支持異步取消 12 13 //連接 BackgroundWorker 對象的處理程序 14 _worker.DoWork += _worker_DoWork; //開始執行後台操作時觸發,即調用 BackgroundWorker.RunWorkerAsync 時觸發 15 _worker.ProgressChanged += _worker_ProgressChanged; //調用 BackgroundWorker.ReportProgress(System.Int32) 時觸發 16 _worker.RunWorkerCompleted += _worker_RunWorkerCompleted; //當後台操作已完成、被取消或引發異常時觸發 17 } 18 19 /// <summary> 20 /// 當後台操作已完成、被取消或引發異常時發生 21 /// </summary> 22 /// <param name="sender"></param> 23 /// <param name="e"></param> 24 private void _worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) 25 { 26 MessageBox.Show(e.Cancelled ? $@"進程已被取消:{progressBar.Value}%" : $@"進程執行完成:{progressBar.Value}%"); 27 progressBar.Value = 0; 28 } 29 30 /// <summary> 31 /// 調用 BackgroundWorker.ReportProgress(System.Int32) 時發生 32 /// </summary> 33 /// <param name="sender"></param> 34 /// <param name="e"></param> 35 private void _worker_ProgressChanged(object sender, ProgressChangedEventArgs e) 36 { 37 progressBar.Value = e.ProgressPercentage; //異步任務的進度百分比 38 } 39 40 /// <summary> 41 /// 開始執行後台操作觸發,即調用 BackgroundWorker.RunWorkerAsync 時發生 42 /// </summary> 43 /// <param name="sender"></param> 44 /// <param name="e"></param> 45 private static void _worker_DoWork(object sender, DoWorkEventArgs e) 46 { 47 var worker = sender as BackgroundWorker; 48 if (worker == null) 49 { 50 return; 51 } 52 53 for (var i = 0; i < 10; i++) 54 { 55 //判斷程序是否已請求取消後台操作 56 if (worker.CancellationPending) 57 { 58 e.Cancel = true; 59 break; 60 } 61 62 worker.ReportProgress((i + 1) * 10); //觸發 BackgroundWorker.ProgressChanged 事件 63 Thread.Sleep(250); //線程掛起 250 毫秒 64 } 65 } 66 67 private void btnDo_Click(object sender, EventArgs e) 68 { 69 //判斷 BackgroundWorker 是否正在執行異步操作 70 if (!_worker.IsBusy) 71 { 72 _worker.RunWorkerAsync(); //開始執行後台操作 73 } 74 } 75 76 private void btnCancel_Click(object sender, EventArgs e) 77 { 78 _worker.CancelAsync(); //請求取消掛起的後台操作 79 } 80 }
圖4-1
入門:《走進異步編程的世界 - 開始接觸 async/await 異步編程》
上篇:《走進異步編程的世界 - 剖析異步方法(上)》《走進異步編程的世界 - 剖析異步方法(下)》