zoukankan      html  css  js  c++  java
  • [.NET] 怎样使用 async & await 一步步将同步代码转换为异步编程

    怎样使用 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.这是右边点击按钮的事件:

     1         /// <summary>
     2         /// 点击事件
     3         /// </summary>
     4         /// <param name="sender"></param>
     5         /// <param name="e"></param>
     6         private void btnSwitch_Click(object sender, RoutedEventArgs e)
     7         {
     8             //清除文本框所有内容
     9             tbResult.Clear();
    10 
    11             //统计总数
    12             SumSizes();
    13         }

      

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

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

        ② GetUrlContents:获取网址内容;

        ③ DisplayResults:显示结果。

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

     1         /// <summary>
     2         /// 统计总数
     3         /// </summary>
     4         private void SumSizes()
     5         {
     6             //加载网址
     7             var urls = InitUrlInfoes();
     8 
     9             //字节总数
    10             var totalCount = 0;
    11             foreach (var url in urls)
    12             {
    13                 //返回一个 url 内容的字节数组
    14                 var contents = GetUrlContents(url);
    15 
    16                 //显示结果
    17                 DisplayResults(url, contents);
    18 
    19                 //更新总数
    20                 totalCount += contents.Length;
    21             }
    22 
    23             tbResult.Text += $"
             Total: {totalCount}, OK!";
    24         }
    View Code

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

     1         /// <summary>
     2         /// 初始化 url 信息列表
     3         /// </summary>
     4         /// <returns></returns>
     5         private IList<string> InitUrlInfoes()
     6         {
     7             var urls = new List<string>()
     8             {
     9                 "http://www.cnblogs.com/",
    10                 "http://www.cnblogs.com/liqingwen/",
    11                 "http://www.cnblogs.com/liqingwen/p/5902587.html",
    12                 "http://www.cnblogs.com/liqingwen/p/5922573.html"
    13             };
    14 
    15             return urls;
    16         }
    View Code

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

     1 /// <summary>
     2         /// 获取网址内容
     3         /// </summary>
     4         /// <param name="url"></param>
     5         /// <returns></returns>
     6         private byte[] GetUrlContents(string url)
     7         {
     8             //假设下载速度平均延迟 300 毫秒
     9             Thread.Sleep(300);
    10 
    11             using (var ms = new MemoryStream())
    12             {
    13                 var req = WebRequest.Create(url);
    14 
    15                 using (var response = req.GetResponse())
    16                 {
    17                     //从指定 url 里读取数据
    18                     using (var rs = response.GetResponseStream())
    19                     {
    20                         //从当前流中读取字节并将其写入到另一流中
    21                         rs.CopyTo(ms);
    22                     }
    23                 }
    24 
    25                 return ms.ToArray();
    26             }
    27 
    28         }
    View Code

      (4)DisplayResults 方法:显示结果

     1         /// <summary>
     2         /// 显示结果
     3         /// </summary>
     4         /// <param name="url"></param>
     5         /// <param name="content"></param>
     6         private void DisplayResults(string url, byte[] content)
     7         {
     8             //内容长度
     9             var bytes = content.Length;
    10 
    11             //移除 http:// 前缀
    12             var replaceUrl = url.Replace("http://", "");
    13 
    14             //显示
    15             tbResult.Text += $"
     {replaceUrl}:   {bytes}";
    16         }
    View Code

    测试结果图

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

      当你点击启动的同时,也就是下载 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(300);

       

      (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)这是修改后的整体方法

     1         /// <summary>
     2         /// 获取网址内容
     3         /// </summary>
     4         /// <param name="url"></param>
     5         /// <returns></returns>
     6         /// <remarks>
     7         /// private async byte[] GetUrlContents(string url)
     8         /// private async Task<byte[]> GetUrlContents(string url)
     9         /// </remarks>
    10         private async Task<byte[]> GetUrlContentsAsync(string url)
    11         {
    12             //假设下载速度平均延迟 300 毫秒
    13             await Task.Delay(300);
    14 
    15             using (var ms = new MemoryStream())
    16             {
    17                 var req = WebRequest.Create(url);
    18 
    19                 //var response = req.GetResponse();
    20                 //Task<WebResponse> responseTask = req.GetResponseAsync();
    21                 //var response = await responseTask;
    22 
    23                 using (var response = await req.GetResponseAsync())
    24                 {
    25                     //从指定 url 里读取数据
    26                     using (var rs = response.GetResponseStream())
    27                     {
    28                         //从当前流中读取字节并将其写入到另一流中
    29                         //rs.CopyTo(ms);
    30                         await rs.CopyToAsync(ms);
    31                     }
    32                 }
    33 
    34                 return ms.ToArray();
    35             }
    36         }
    GetUrlContentsAsync 方法

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

     1         /// <summary>
     2         /// 异步统计总数
     3         /// </summary>
     4         private async Task SumSizesAsync()
     5         {
     6             //加载网址
     7             var urls = InitUrlInfoes();
     8 
     9             //字节总数
    10             var totalCount = 0;
    11             foreach (var url in urls)
    12             {
    13                 //返回一个 url 内容的字节数组
    14                 var contents = await GetUrlContentsAsync(url);
    15 
    16                 //显示结果
    17                 DisplayResults(url, contents);
    18 
    19                 //更新总数
    20                 totalCount += contents.Length;
    21             }
    22 
    23             tbResult.Text += $"
             Total: {totalCount}, OK!";
    24         }

      3.再修改下 btnSwitch_Click

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

     1         /// <summary>
     2         /// 异步点击事件
     3         /// </summary>
     4         /// <param name="sender"></param>
     5         /// <param name="e"></param>
     6         private async void btnSwitch_Click(object sender, RoutedEventArgs e)
     7         {
     8             btnSwitch.IsEnabled = false;
     9 
    10             //清除文本框所有内容
    11             tbResult.Clear();
    12 
    13             //统计总数
    14             await SumSizesAsync();
    15 
    16             btnSwitch.IsEnabled = true;
    17         }

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

      var hc = new HttpClient() { MaxResponseContentBufferSize = 1024000 };
    
      //var contents = await GetUrlContentsAsync(url);  
      var contents = await hc.GetByteArrayAsync(url);
     1         /// <summary>
     2         /// 异步统计总数
     3         /// </summary>
     4         private async Task SumSizesAsync()
     5         {
     6 
     7             var hc = new HttpClient() { MaxResponseContentBufferSize = 102400 };
     8             //加载网址
     9             var urls = InitUrlInfoes();
    10 
    11             //字节总数
    12             var totalCount = 0;
    13             foreach (var url in urls)
    14             {
    15                 //返回一个 url 内容的字节数组
    16                 //var contents = await GetUrlContentsAsync(url);
    17                 var contents = await hc.GetByteArrayAsync(url);
    18 
    19                 //显示结果
    20                 DisplayResults(url, contents);
    21 
    22                 //更新总数
    23                 totalCount += contents.Length;
    24             }
    25 
    26             tbResult.Text += $"
             Total: {totalCount}, OK!";
    27         }
    修改后的: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

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

  • 相关阅读:
    小知识:关于String的创建
    JSON学习笔记,数组
    JSON学习笔记,对象
    爱乐之城 La La Land
    TwoSampleMR包出现报错: None of the specified columns present
    无亲缘关系为何IBD结果为同卵双胞胎/重复样本
    本周最新文献速递20210321
    本周最新文献速递20210314
    使用 KaKs Calculator 计算 KaKs 的衍生问题解答
    本周最新文献速递20210307
  • 原文地址:https://www.cnblogs.com/liqingwen/p/6079707.html
Copyright © 2011-2022 走看看