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

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

  • 相关阅读:
    hdu1238 Substrings
    CCF试题:高速公路(Targin)
    hdu 1269 迷宫城堡(Targin算法)
    hdu 1253 胜利大逃亡
    NYOJ 55 懒省事的小明
    HDU 1024 Max Sum Plus Plus
    HDU 1087 Super Jumping! Jumping! Jumping!
    HDU 1257 最少拦截系统
    HDU 1069 Monkey and Banana
    HDU 1104 Remainder
  • 原文地址:https://www.cnblogs.com/liqingwen/p/6079707.html
Copyright © 2011-2022 走看看