如何使用Async&Await将同步代码转换为异步编程

Posted DotNet

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何使用Async&Await将同步代码转换为异步编程相关的知识,希望对你有一定的参考价值。


来源:反骨仔(二五仔)

链接:cnblogs.com/liqingwen/p/6079707.html


介绍


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



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

 

添加引用


如何使用Async&Await将同步代码转换为异步编程


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 = 0;

            foreach (var url in urls)

            {

                //返回一个 url 内容的字节数组

                var contents = GetUrlContents(url);

                //显示结果

                DisplayResults(url, contents);

                //更新总数

                totalCount += contents.Length;

            }

            tbResult.Text += $"         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(300);

            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 += $" {replaceUrl}:   {bytes}";

}


如何使用Async&Await将同步代码转换为异步编程


全部显示需要耗费数秒的时间。在点击的同时,它在等待请求资源下载时,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)这是修改后的整体方法


       /// <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(300);

            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();

            }

        }


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


       /// <summary>

        /// 异步统计总数

        /// </summary>

        private async Task SumSizesAsync()

        {

            //加载网址

            var urls = InitUrlInfoes();

            //字节总数

            var totalCount = 0;

            foreach (var url in urls)

            {

                //返回一个 url 内容的字节数组

                var contents = await GetUrlContentsAsync(url);

                //显示结果

                DisplayResults(url, contents);

                //更新总数

                totalCount += contents.Length;

            }

            tbResult.Text += $"         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 = 1024000 };

  //var contents = await GetUrlContentsAsync(url);  

  var contents = await hc.GetByteArrayAsync(url);


 /// <summary>

/// 异步统计总数

/// </summary>

private async Task SumSizesAsync()

{

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

            //加载网址

            var urls = InitUrlInfoes();

            //字节总数

            var totalCount = 0;

            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 += $"         Total: {totalCount}, OK!";

}


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



修改后的效果差异:最重要的是,UI 线程不会阻塞下载过程。当 web 资源下载、统计并显示时,可以移动或调整窗口的大小。如果其中一个网站速度或不响应,可以通过选择关闭按钮取消了操作 (右上角的 X)。

      

Demo 下载(https://git.oschina.net/liqingwen/cnblogsDemo/tree/master)


系列文章


《利用 async & await 的异步编程》






关注「DotNet」 

看更多精选 .Net 技术文章

↓↓↓

以上是关于如何使用Async&Await将同步代码转换为异步编程的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 async/await 处理同步方法/任务

demo_09 async&await

如何使用 async/await 处理错误?

使用async和await将同步方法包装成异步方法

利用 async & await 的异步编程

异步编程之 async 和 await