怎样使用 async & await 一步步将同步代码转换为异步编程

【博主】反骨仔    【出处】http://www.cnblogs.com/liqingwen/p/6079707.html   

  上次,博主通过《利用 async & await 的异步编程》该篇点睛之作介绍了 async & await 的基本用法及异步的控制流和一些其它的东西。

  今天,博主打算从创建一个普通的 WPF 应用程序开始,看看如何将它逐步转换成一个异步的解决方案。

目录

介绍

  这里通过一个普通的 WPF 程序进行讲解:

  只是一个文本框和一个按钮,左边文本框的内容为点击右键按钮时所产生的结果。

添加引用

  demo 可能需要用到的部分 using 指令:

using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;

先创建一个同步的 WPF

  1.这是右边点击按钮的事件:

         /// <summary>
/// 点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSwitch_Click(object sender, RoutedEventArgs e)
{
//清除文本框所有内容
tbResult.Clear(); //统计总数
SumSizes();
}

  

  2.我在 SumSizes 方法内包含几个方法:

    ① InitUrlInfoes:初始化 url 信息列表;

    ② GetUrlContents:获取网址内容;

    ③ DisplayResults:显示结果。

  (1)SumSizes 方法:统计总数。

         /// <summary>
/// 统计总数
/// </summary>
private void SumSizes()
{
//加载网址
var urls = InitUrlInfoes(); //字节总数
var totalCount = ;
foreach (var url in urls)
{
//返回一个 url 内容的字节数组
var contents = GetUrlContents(url); //显示结果
DisplayResults(url, contents); //更新总数
totalCount += contents.Length;
} tbResult.Text += $"\r\n Total: {totalCount}, OK!";
}

  (2)InitUrlInfoes 方法:初始化 url 信息列表。

         /// <summary>
/// 初始化 url 信息列表
/// </summary>
/// <returns></returns>
private IList<string> InitUrlInfoes()
{
var urls = new List<string>()
{
"http://www.cnblogs.com/",
"http://www.cnblogs.com/liqingwen/",
"http://www.cnblogs.com/liqingwen/p/5902587.html",
"http://www.cnblogs.com/liqingwen/p/5922573.html"
}; return urls;
}

  (3)GetUrlContents 方法:获取网址内容。

 /// <summary>
/// 获取网址内容
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private byte[] GetUrlContents(string url)
{
//假设下载速度平均延迟 300 毫秒
Thread.Sleep(); using (var ms = new MemoryStream())
{
var req = WebRequest.Create(url); using (var response = req.GetResponse())
{
//从指定 url 里读取数据
using (var rs = response.GetResponseStream())
{
//从当前流中读取字节并将其写入到另一流中
rs.CopyTo(ms);
}
} return ms.ToArray();
} }

  (4)DisplayResults 方法:显示结果

         /// <summary>
/// 显示结果
/// </summary>
/// <param name="url"></param>
/// <param name="content"></param>
private void DisplayResults(string url, byte[] content)
{
//内容长度
var bytes = content.Length; //移除 http:// 前缀
var replaceUrl = url.Replace("http://", ""); //显示
tbResult.Text += $"\r\n {replaceUrl}: {bytes}";
}

测试结果图

  界面上的内容显示需要花费一定的时间(可能是数秒)。

  当你点击启动的同时,也就是下载 url 内容的时候,即等待资源的一个过程,此时,UI 线程会进行阻塞。因为在等待资源的这一个期间,我们无法对 UI 进行其他操作,如:移动、最大、最小和关闭窗口等操作。这样会令用户非常反感,特别是时间一长的时候,就会出现界面尚未响应,并且下载时候(网站没有响应或响应时间过长),也无法凸显站点失败的有效信息。

  在 UI 阻塞的同时,想关闭也是一件挺麻烦的事情,我想,通过任务管理器进行关闭也许是一件比较正确的形式吧。

将上面的 demo 逐步转换为异步方法

  1.GetUrlContents 方法 => GetUrlContentsAsync 异步方法

  (1) 将 GetResponse 方法改成 GetResponseAsync 方法:

  //var response = req.GetResponse();
var response = req.GetResponseAsync()

  

  (2)在 GetResponseAsync 方法前加上 await:

  GetResponseAsync 将返回 Task。 在这种情况下,任务返回变量 TResult,具有类型 WebResponse

  从任务若要检索 WebResponse 值,将 await 运算符应用于调用的 GetResponseAsync 方法。

  //var response = req.GetResponseAsync()
var response = await req.GetResponseAsync()

  await 运算符挂起当前方法,直到等待的任务完成。同时,控制权返回到当前方法的调用方。在这里,当前方法是 GetUrlContents,因此,调用方是 SumSizes。当任务完成时,将提交的 WebResponse 对象生成,将等待的任务的值分配给 response。

  上面的内容也可以拆分成下面的内容:

  //Task<WebResponse> responseTask = req.GetResponseAsync();
//var response = await responseTask;

  responseTask 为 webReq.GetResponseAsync 的调用返回 Task 或 Task<WebResponse>。 然后 await 运算符应用于 task 检索 WebResponse 值。

  

  (3)由于在上一步中添加了 await 运算符,编译器会报告错误。await 运算符在标有 async 的方法下才能使用。当您重复转换步骤替换 CopyTo 为 CopyToAsync 时,请先暂时忽略该错误。

  • 更改调用 CopyToAsync方法的名称。

  • CopyTo 或 CopyToAsync 方法复制字节为其参数,不返回有意义的值。在同步版本中,CopyTo 的调用不返回值。在异步版本中,即CopyToAsync,返回 Task,可应用 await 于方法 CopyToAsync。

  //rs.CopyTo(ms);
await rs.CopyToAsync(ms);

  

  (4)也要修改 Tread.Sleep。Thread.Sleep 是同步延迟,Task.Delay 异步延迟;Thread.Sleep 会阻塞线程,而Task.Delay 不会。

  //Thread.Sleep(300);
await Task.Delay();

  

  (5)在 GetUrlContents 仍然要修改的只是调整方法签名。在标有异步的方法只能使用 await 运算符 async 修饰符。添加 async 修饰符标记方法作为异步方法 。

  //private async byte[] GetUrlContents(string url)
//private async Task<byte[]> GetUrlContents(string url)
private async Task<byte[]> GetUrlContentsAsync(string url)

  异步方法的返回类型只能 Task<T>、Task 或 void。 通常 void 的返回类型仅在异步事件处理程序中使用。在某些情况下,您使用 Task<T>,如果返回类型 T 的值的完整方法具有 return 语句以及使用 Task,但是已完成方法不返回有意义的值。可以将 Task 返回类型理解为“任务 (失效)”。

  方法 GetURLContents 具有返回语句,因此,该语句返回字节数组。 这里,异步版本的返回类型为 Task<T>,T 为字节数组。在方法签名中进行以下更改:

  • 返回类型更改 Task<byte[]>。

  • 按照约定,异步方法是以“Async”结尾的名称,因此可对方法 GetURLContentsAsync 重命名。

  (6)这是修改后的整体方法

         /// <summary>
/// 获取网址内容
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
/// <remarks>
/// private async byte[] GetUrlContents(string url)
/// private async Task<byte[]> GetUrlContents(string url)
/// </remarks>
private async Task<byte[]> GetUrlContentsAsync(string url)
{
//假设下载速度平均延迟 300 毫秒
await Task.Delay(); using (var ms = new MemoryStream())
{
var req = WebRequest.Create(url); //var response = req.GetResponse();
//Task<WebResponse> responseTask = req.GetResponseAsync();
//var response = await responseTask; using (var response = await req.GetResponseAsync())
{
//从指定 url 里读取数据
using (var rs = response.GetResponseStream())
{
//从当前流中读取字节并将其写入到另一流中
//rs.CopyTo(ms);
await rs.CopyToAsync(ms);
}
} return ms.ToArray();
}
}

GetUrlContentsAsync 方法

  2.仿造上述过程将 SumSizes 方法 => SumSizesAsync 异步方法。

         /// <summary>
/// 异步统计总数
/// </summary>
private async Task SumSizesAsync()
{
//加载网址
var urls = InitUrlInfoes(); //字节总数
var totalCount = ;
foreach (var url in urls)
{
//返回一个 url 内容的字节数组
var contents = await GetUrlContentsAsync(url); //显示结果
DisplayResults(url, contents); //更新总数
totalCount += contents.Length;
} tbResult.Text += $"\r\n Total: {totalCount}, OK!";
}

  3.再修改下 btnSwitch_Click

  这里为防止意外地重新输入操作,先在顶部禁用按钮,在最终完成时再启用按钮。通常,不更改事件处理程序的名称。 因为事件处理程序不需要返回值,所以返回类型也不需要更改为 Task。

         /// <summary>
/// 异步点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void btnSwitch_Click(object sender, RoutedEventArgs e)
{
btnSwitch.IsEnabled = false; //清除文本框所有内容
tbResult.Clear(); //统计总数
await SumSizesAsync(); btnSwitch.IsEnabled = true;
}

  4.其实可以采用 .NET 自带的 GetByteArrayAsync 异步方法替换我们自己写的 GetUrlContentsAsync 异步方法,之前只是为了演示的需要。

  var hc = new HttpClient() { MaxResponseContentBufferSize =  };

  //var contents = await GetUrlContentsAsync(url);
var contents = await hc.GetByteArrayAsync(url);
         /// <summary>
/// 异步统计总数
/// </summary>
private async Task SumSizesAsync()
{ var hc = new HttpClient() { MaxResponseContentBufferSize = };
//加载网址
var urls = InitUrlInfoes(); //字节总数
var totalCount = ;
foreach (var url in urls)
{
//返回一个 url 内容的字节数组
//var contents = await GetUrlContentsAsync(url);
var contents = await hc.GetByteArrayAsync(url); //显示结果
DisplayResults(url, contents); //更新总数
totalCount += contents.Length;
} tbResult.Text += $"\r\n Total: {totalCount}, OK!";
}

修改后的:SumSizesAsync 方法

  这时,项目的变换从同步到异步操作已经完成。

  修改后的效果差异:最重要的是,UI 线程不会阻塞下载过程。当 web 资源(或其他资源)下载、统计并显示时,可以移动或调整窗口的大小。如果其中一个网站速度或不响应,你可以直接点击关闭 (右上角的 X),再也不需要打开任务管理器进行关闭该进程了。

Demo 下载

同系列的随笔


【参考】https://docs.microsoft.com/en-us/dotnet/articles/csharp/programming-guide/concepts/async/walkthrough-accessing-the-web-by-using-async-and-await

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

[.NET] 怎样使用 async & await 一步步将同步代码转换为异步编程的更多相关文章

  1. 【TypeScript】如何在TypeScript中使用async/await,让你的代码更像C#。

    [TypeScript]如何在TypeScript中使用async/await,让你的代码更像C#. async/await 提到这个东西,大家应该都很熟悉.最出名的可能就是C#中的,但也有其它语言也 ...

  2. Async/Await是这样简化JavaScript代码的

    译者按: 在Async/Await替代Promise的6个理由中,我们比较了两种不同的异步编程方法:Async/Await和Promise,这篇博客将通过示例代码介绍Async/Await是如何简化J ...

  3. [译]async/await中使用阻塞式代码导致死锁 百万数据排序:优化的选择排序(堆排序)

    [译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的 ...

  4. [译]async/await中使用阻塞式代码导致死锁

    原文:[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Clea ...

  5. [.NET] 利用 async & await 进行异步 IO 操作

    利用 async & await 进行异步 IO 操作 [博主]反骨仔 [出处]http://www.cnblogs.com/liqingwen/p/6082673.html  序 上次,博主 ...

  6. [C#] 走进异步编程的世界 - 开始接触 async/await

    走进异步编程的世界 - 开始接触 async/await 序 这是学习异步编程的入门篇. 涉及 C# 5.0 引入的 async/await,但在控制台输出示例时经常会采用 C# 6.0 的 $&qu ...

  7. C#~异步编程再续~await与async引起的w3wp.exe崩溃

    返回目录 最近怪事又开始发生了,IIS的应用程序池无做挂掉,都指向同一个矛头,async,threadPool,Task,还有一个System.NullReferenceException,所以这些都 ...

  8. 走进异步编程的世界 - 开始接触 async/await

    [C#] 走进异步编程的世界 - 开始接触 async/await   走进异步编程的世界 - 开始接触 async/await 序 这是学习异步编程的入门篇. 涉及 C# 5.0 引入的 async ...

  9. [C#] 走进异步编程的世界 - 开始接触 async/await(转)

    原文链接:http://www.cnblogs.com/liqingwen/p/5831951.html 走进异步编程的世界 - 开始接触 async/await 序 这是学习异步编程的入门篇. 涉及 ...

随机推荐

  1. a标签点击跳转失效--IE6、7的奇葩bug

    一般运用a标签包含img去实现点击图片跳转的功能,这是前端经常要用到的东西. 今天遇到个神奇的bug:如果在img上再包裹一层div,而且div设置了width和height,则图片区域点击时,无任何 ...

  2. ABP入门系列(2)——通过模板创建MAP版本项目

    一.从官网创建模板项目 进入官网下载模板项目 依次按下图选择: 输入验证码开始下载 下载提示: 二.启动项目 使用VS2015打开项目,还原Nuget包: 设置以Web结尾的项目,设置为启动项目: 打 ...

  3. nodejs进阶(2)—函数模块调用

    函数调用 1. 文件内普通函数调用 创建一个js文件命名为2_callFunction.js,其中定义一个函数fun1,向返回对象输出了一段字符串“你好,我是fun1”. //------------ ...

  4. DDD 领域驱动设计-商品建模之路

    最近在做电商业务中,有关商品业务改版的一些东西,后端的架构设计采用现在很流行的微服务,有关微服务的简单概念: 微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成.系统中的各个微服务可被独 ...

  5. iOS controller解耦探究实现——第一次写博客

    大学时曾经做过android的开发,目前的工作是iOS的开发.之前自己记录东西都是通过自己比较喜欢的笔记类的应用记录下了.直到前段时一个哥们拉着我注册了一个博客.现在终于想明白了,博客这个东西受众会稍 ...

  6. Autofac - MVC/WebApi中的应用

    Autofac前面写了那么多篇, 其实就是为了今天这一篇, Autofac在MVC和WebApi中的应用. 一.目录结构 先看一下我的目录结构吧, 搭了个非常简单的架构, IOC(web), IBLL ...

  7. BPM配置故事之案例7-公式计算

    行政主管发来邮件.要求物资明细表增加"单价""总价"."单价"由其审批时填写,"总价"根据"单价"与 ...

  8. 如何解决流程开发中SheetRadioButtonList页面取值问题

    分享一个常见的取值问题. 应用场景: SheetRadioButtonList控件,点击其中一项执行事件操作.如果是页面加载的情况下,值就无法取到. 具体原因如下: 我给SheetRadioButto ...

  9. CentOS7之按时间段截取指定的Tomcat日志到指定文件的方法

    CentOS7之按时间段截取指定的Tomcat日志到指定文件的方法 sed -n '/2016-11-02 15:00:/,/2016-11-02 15:05:/p' catalina.out > ...

  10. NV显卡Ubuntu14.04更新软件导致登录死循环,不过可以进入tty模式

    注意:此方法只适用于nv显卡的电脑! 在网上寻找各种方法无果的情况下,选择重新安装显卡驱动,成功登录进入图形界面. 一.首先需要在另外一台电脑(windows系统也可以)上下载NVIDIA相应显卡驱动 ...