的异步编程,中执行异步操作
分类:计算机编程

走进异步编程的世界 - 在 GUI 中执行异步操作

利用 async & await 的异步编程

【博主】反骨仔    【出处】   

【博主】反骨仔  【原文地址】http://www.cnblogs.com/liqingwen/p/5877042.html 

目录

  • 异步编程的简介
  • 异步提高响应能力
  • 更容易编写的异步方法
  • 异步方法的控制流(核心)
  • 异步中的线程
  • async 和 await 修饰符
  • 返回类型和参数信息
  • 命名的约定

 

   这是继《[开始接触 async/await

一、异步编程的简介

  通过使用异步编程,你可以避免性能瓶颈并增强你的应用程序的总体响应能力。

  从 VS 2012 开始,新引入了一个简化的方法,称为异步编程。我们在 >= .NET 4.5 中和 Windows 运行时中使用异步,编译器它会帮助了我们降低了曾经进行的高难度异步代码编写的工作,但逻辑结构却类似于同步代码。因此,我们仅需要进行一小部分编程的工作就可以获得异步编程的所有优点。

 

异步编程](

剖析异步方法]( WinForm 中如何执行异步操作。

 

二、异步提高响应能力

  异步对可能引起阻塞的活动(如访问 Web 时),对 Web 资源的访问有时过慢或延迟过高。若这种任务在同步过程中受阻,则整个应用程序必须等待响应完成。 在使用异步的过程中,我们的应用程序可继续执行不依赖 Web 资源的其他工作,并会一直等待阻塞的任务顺利完成。

  这是一些典型的使用异步的应用场景,以及一些在 .NET >= 4.5 后新增的类库。

图片 1

  所有与用户界面相关的操作通常共享一个线程,所以使用异步对于使用 UI 线程的 App 来说是非常重要的。

  如果说你的 App 所有操作都是同步的,也就是说,当一个线程出现阻塞,其它线程都会出现阻塞,更严重的是, App 会停止响应。

图片 2

 

  使用异步方法时,App 将继续响应 UI。如:最大和最小化,但是功能依然在后台执行(如:下载)。

 

目录

  • 在 WinForm 中执行异步操作
  • 在 WinForm 中使用异步 Lambda 表达式
  • 一个完整的 WinForm 程序
  • 另一种异步方式 - BackgroundWorker 类

 

三、更容易编写的异步方法

  C# 中的 async 和 await 关键字都是异步编程的核心。通过使用这两个关键字,我们就可以在 .NET 轻松创建异步方法。

  示例:

 1         /// <summary>
 2         /// 异步访问 Web 
 3         /// </summary>
 4         /// <returns></returns>
 5         /// <remarks>
 6         /// 方法签名的 3 要素:
 7         ///     ① async 修饰符
 8         ///     ② 返回类型 Task 或 Task<TResult>:这里的 Task<int> 表示 return 语句返回 int 类型
 9         ///     ③ 方法名以 Async 结尾
10         /// </remarks>
11         async Task<int> AccessTheWebAsync()
12         {
13             //记得 using System.Net.Http 哦
14             var client = new HttpClient();
15 
16             //执行异步方法 GetStringAsync
17             Task<string> getStringTask = client.GetStringAsync("http://www.google.com.hk/");
18 
19             //假设在这里执行一些非异步的操作
20             Do();
21 
22             //等待操作挂起方法 AccessTheWebAsync
23             //直到 getStringTask 完成,AccessTheWebAsync 方法才会继续执行
24             //同时,控制将返回到 AccessTheWebAsync 方法的调用方
25             //直到 getStringTask 完成后,将在这里恢复控制。
26             //然后从 getStringTask 拿到字符串结果
27             string urlContents = await getStringTask;
28 
29             //返回字符串的长度(int 类型)
30             return urlContents.Length;
31         }

 

  如果 AccessTheWebAsync 在调用 GetStringAsync() 时没有其它操作(如:代码中的 Do()),你可以用这样的方式来简化代码。

string urlContents = await client.GetStringAsync("http://www.google.com.hk/");

  

  简单总结:

  (1)方法签名包含一个 async 修饰符。

  (2)根据约定,异步方法的名称需要以“Async”后缀为结尾。

  (3)3 种返回类型:

    ① Task<TResult>:返回 TResult 类型。

    ② Task:没有返回值,即返回值为 void。

    ③ void:只适用于异步事件处理程序。

  (4)方法通常包含至少一个 await 表达式,该表达式标记一个点,我们可以成为悬挂点,在该点上,直到等待的异步操作完成,之后的方法才能继续执行。 与此同时,该方法将挂起,并将控制权返回到方法的调用方。

  

  需要使用异步方法的话,我们直接在系统内部使用所提供的关键字 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     }

  可是执行结果却是:

图片 3

图1-1

 

  【发现的问题】

    ①好像没有变成“Doing”?

    ②并且拖动窗口的时候卡住不动了?

    ③3秒后突然变到想拖动到的位置?

    ④同时文本变成“Complete”?

 

  【分析】GUI 程序在设计中要求所有的显示变化都必须在主 GUI 线程中完成,如点击事件和移动窗体。Windows 程序时通过 消息来实现,消息放入消息泵管理的消息队列中。点击按钮时,按钮的Click消息放入消息队列。消息泵从队列中移除该消息,并开始处理点击事件的代码,即 btnDo_Click 事件的代码。

  btnDo_Click 事件会将触发行为的消息放入队列,但在 btnDo_Click 时间处理程序完全退出前(线程挂起 3 秒退出前),消息都无法执行。(3 秒后)接着所有行为都发生了,但速度太快肉眼无法分辨才没有发现标签改成“Doing”。

图片 4

图1-2 点击事件

图片 5

图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     }

图片 6

图1-4

  现在,就是原先希望看到的效果。

  【分析】btnDo_Click 事件处理程序先将前两条消息压入队列,然后将自己从处理器移出,在3秒后(等待空闲任务完成后 Task.Delay )再将自己压入队列。这样可以保持响应,并保证所有的消息可以在线程挂起的时间内被处理。

 

四、异步方法的控制流(核心)

  异步编程中最重要却不易懂的是控制流,即不同方法间的切换。现在,请用一颗感恩的心来观察下图。

图片 7

  步骤解析:

  ① 事件处理程序调用并等待 AccessTheWebAsync() 异步方法。

  ② AccessTheWebAsync 创建 HttpClient 对象并调用它的 GetStringAsync 异步方法来下载网站内容。

  ③ 假设 GetStringAsync 中发生了某种情况,该情况挂起了它的进程。可能必须等待网站下载或一些其他阻塞的活动。为避免阻塞资源,GetStringAsync() 会将控制权出让给其调用方 AccessTheWebAsync。GetStringAsync 返回 Task,其中 TResult 为字符串,并且 AccessTheWebAsync 将任务分配给 getStringTask 变量。该任务表示调用 GetStringAsync 的正在进行的进程,其中承诺当工作完成时产生实际字符串值。

  ④ 由于尚未等待 getStringTask,因此,AccessTheWebAsync 可以继续执行不依赖于 GetStringAsync 得出最终结果的其他任务。该任务由对同步方法 DoIndependentWork 的调用表示。

  ⑤ DoIndependentWork 是完成其工作并返回其调用方的同步方法。

  ⑥ AccessTheWebAsync 已完成工作,可以不受 getStringTask 的结果影响。 接下来,AccessTheWebAsync 需要计算并返回该下载字符串的长度,但该方法仅在具有字符串时才能计算该值。因此,AccessTheWebAsync 使用一个 await 运算符来挂起其进度,并把控制权交给调用 AccessTheWebAsync 的方法。AccessTheWebAsync 将 Task<int> 返回至调用方。 该任务表示对产生下载字符串长度的整数结果的一个承诺。

  【备注】如果 GetStringAsync(即 getStringTask)在 AccessTheWebAsync 等待前完成,则控制权会保留在 AccessTheWebAsync 中。 如果异步调用过程 (getStringTask) 已完成,并且 AccessTheWebSync 不必等待最终结果,则挂起然后返回到 AccessTheWebAsync,但这会造成成本的浪费。

  在调用方内部(假设这是一个事件处理程序),处理模式将继续。在等待结果前,调用方可以开展不依赖于 AccessTheWebAsync 结果的其他工作,否则就需等待片刻。事件处理程序等待 AccessTheWebAsync,而 AccessTheWebAsync 等待 GetStringAsync。

  ⑦ GetStringAsync 完成并生成一个字符串结果。 字符串结果不是通过你预期的方式调用 GetStringAsync 所返回的。(请记住,此方法已在步骤 3 中返回一个任务。)相反,字符串结果存储在表示完成方法 getStringTask 的任务中。 await 运算符从 getStringTask 中检索结果。赋值语句将检索到的结果赋给 urlContents。

  ⑧ 当 AccessTheWebAsync 具有字符串结果时,该方法可以计算字符串长度。然后,AccessTheWebAsync 工作也将完成,并且等待事件处理程序可继续使用。 

 

  你可以尝试思考一下同步行为和异步行为之间的差异。当其工作完成时(第 5 步)会返回一个同步方法,但当其工作挂起时(第 3 步和第 6 步),异步方法会返回一个任务值。在异步方法最终完成其工作时,任务会标记为已完成,而结果(如果有)将存储在任务中。

 

 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         /// <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     }

图片 8

 图1.1-1

  上述代码每执行1000次循环就调用 Task.Yield 方法创建一个等待任务,让处理器有时间处理其它任务。该方法在 GUI 程序中是比较有用的。

 

五、异步中的线程

  异步方法旨在成为非阻塞操作。异步方法中的 await 表达式在等待的任务执行的同时不会阻塞当前线程。相反,await 表达式在继续执行时方法的其余部分并将控制权返回到异步方法的调用方。

  async 和 await 关键字不会导致创建其他线程。因为异步方法不会在其自身线程上运行,因此它不需要多线程。只有当方法处于活动状态时,该方法将在当前同步上下文中运行并使用线程上的时间。可以使用 Task.Run 将占用大量 CPU 的工作移到后台线程,但是后台线程不会帮助正在等待结果的进程变为可用状态。

  对于异步编程而言,该基于异步的方法优于几乎每个用例中的现有方法。具体而言,此方法比 BackgroundWorker 更适用于 IO 绑定的操作,因为此代码更简单且无需防止抢先争用条件。结合 Task.Run() 使用时,异步编程比 BackgroundWorker 更适用于 CPU 绑定的操作,因为异步编程将运行代码的协调细节与 Task.Run 传输至线程池的工作区分开来。

 

二、在 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     }

  还是原来的配方,还是熟悉的味道,还是原来哪个窗口,变的只是内涵。

图片 9

图2-1

 

六、async 和 await 修饰符

  当你使用 async 修饰符指定该方法为异步方法时:

  • 可以使用 await 来指定悬挂点。await 运算符会告诉编译器,异步方法只有直到等待的异步过程执行完成,才能继续通过该点往下执行。同时,控制权将返回至异步方法的调用方。await 表达式中异步方法在挂起后,如果该方法还没有执行完成并退出,finally 块中的将不会执行。

  • 标记的异步方法本身可以通过调用它的方法进行等待。异步方法中通常包含一个或多个 await 运算符,当然,一个 await 表达式都不存在也不会导致编译器错误,但是编译器会发出警告,该方法在执行的时候依然会依照同步方法来执行,async 其实只是一个标识的作用而已,告诉编译器他“应该”是一个异步方法。

 

三、一个完整的 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         /// <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     }

图片 10

 图3-1

 

七、返回类型和参数信息

  在编写异步方法时,我们绝大部分会使用 Task 和 Task<TResult> 作为返回类型。

 

  示例:

 1         static async Task<Guid> Method1Async()  //Task<Guid>
 2         {
 3             var result = Guid.NewGuid();
 4 
 5             await Task.Delay(1);
 6 
 7             //这里返回一个 Guid 的类型
 8             return result;
 9         }
10 
11         static async Task Method2Async()  //Task
12         {
13             //Do...
14 
15             await Task.Delay(1);
16 
17             //Do...
18 
19             //这里没有 return 语句
20         }

 1             //调用 Method1Async
 2             //方式一
 3             Task<Guid> t1 = Method1Async();
 4             Guid guid1 = t1.Result;
 5 
 6             //方式二
 7             Guid guid2 = await Method1Async();
 8 
 9             //调用 Method2Async
10             //方式一
11             Task t2 = Method2Async();
12             await t2;
13 
14             //方式二
15             await Method2Async();

  每个返回的任务表示正在进行的工作。任务可封装有关异步进程状态的信息,如果未成功,则最后会封装来自进程的最终结果,或者是由该进程引发的异常。

 

  【疑问】那么 void 返回类型是在什么情况下才使用的呢?

  主要用于异步的事件处理程序,异步事件处理程序通常作为异步程序的起始点。void 返回类型告诉了编译器,无需对他进行等待,并且,对于 void 返回类型的方法,我们也无法对他进行异常的捕捉。

 

  异步方法不能够在参数中声明与使用 ref 和 out 关键字,但是异步方法可以调用包含这些参数的方法。

 

四、另一种异步方式 - 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         /// <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     }

图片 11

图4-1

 

八、命名的约定

  根据约定,使用 async 的方法都应该以“Async”作为后缀,如:DownloadAsync() 。但是,如果某一约定中的事件、基类或接口有其他的形式约定,则可以忽略上述约定。例如,不应该修改或重命名常用事件处理程序,如 btnOpen_Click。

 

 传送门

  入门:《[走进异步编程的世界

  • 开始接触 async/await 异步编程](

  上篇:《走进异步编程的世界 - 剖析异步方法(上)》《[走进异步编程的世界

  • 剖析异步方法(下)](

 

 


传送门 

  1. 走进异步编程的世界 - 开始接触 async/await(推荐)

  2. 走进异步编程的世界 - 剖析异步方法(上)

  3. 走进异步编程的世界 - 剖析异步方法(下)

  4. 走进异步编程的世界 - 在 GUI 中执行异步操作

 


【参考引用】微软官方文档图片

【参考】

 

【参考】《Illustrated C# 2012》

本文由pc28.am发布于计算机编程,转载请注明出处:的异步编程,中执行异步操作

上一篇:编码格式篇,编码格式的历史 下一篇:没有了
猜你喜欢
热门排行
精彩图文