走进异步编程的世界 - 在 GUI 中执行异步操作
【博主】反骨仔 【原文地址】
序
这是继《》、《》后的第三篇。主要介绍在 WinForm 中如何执行异步操作。
目录
一、在 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 )再将自己压入队列。这样可以保持响应,并保证所有的消息可以在线程挂起的时间内被处理。
1.1 Task.Yield
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 ///18 /// 循环19 /// 20 /// 21 private static void Loop(int num)22 {23 for (var i = 0; i < num; i++) ;24 }25 }26 27 internal static class DoStuff28 {29 public static async Task 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 程序中是比较有用的。
二、在 WinForm 中使用异步 Lambda 表达式
将刚才的窗口程序的点击事件稍微改动一下。
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
三、一个完整的 WinForm 程序
现在在原来的基础上添加了进度条,以及取消按钮。
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 ///12 /// Do 按钮事件13 /// 14 /// 15 /// 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 try35 {36 await Task.Delay(500, _token);37 completedPercent = (i + 1) * timePercent;38 }39 catch (Exception)40 {41 completedPercent = i * timePercent;42 }43 finally44 {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 ///58 /// 初始化窗体的工具控件59 /// 60 private void InitTool()61 {62 progressBar.Value = 0;63 btnDo.Enabled = true;64 btnCancel.Enabled = true;65 }66 67 ///68 /// 取消事件69 /// 70 /// 71 /// 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
四、另一种异步方式 - BackgroundWorker 类
与 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 ///20 /// 当后台操作已完成、被取消或引发异常时发生21 /// 22 /// 23 /// 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 ///31 /// 调用 BackgroundWorker.ReportProgress(System.Int32) 时发生32 /// 33 /// 34 /// 35 private void _worker_ProgressChanged(object sender, ProgressChangedEventArgs e)36 {37 progressBar.Value = e.ProgressPercentage; //异步任务的进度百分比38 }39 40 ///41 /// 开始执行后台操作触发,即调用 BackgroundWorker.RunWorkerAsync 时发生42 /// 43 /// 44 /// 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
传送门
入门:《》
上篇:《》《》