韦_恩带你用好async/await异步多线程(C#5.0引入的特性)

周围很多人会有这样的问题:

我为什么要用异步?

我为什么要用多线程?

我之前不知道这个,写的程序不也挺好的么?

有这种想法的同学也不奇怪,因为正常的程序当计算量不大或是不需要耗时的数据交互的时候,用不用意义确实不大(比如单纯cad画图方面的开发等),这个原因可能是大部分的情况;当然还有一小部分人是压根不去管性能问题,也不去考虑体验问题,一句话:“能用就好”!emmm.....这样的话,也真的不好说啥(不能对牛弹琴!!!)。

不废话了,当你看到这篇文章时,就证明你已经有想法要变革现有软件了,这很nice!相信我,看完这篇文章你一定会受益匪浅!


目录

1.基础概念--进程、线程

2.异步与多线程啥关系

3.C#中的async与await的使用

3.1异步能缩短数据处理时间么?

 ①同步测试

②异步测试

3.2如何提高效率、更好的利用计算机资源进行数据处理呢?(并行)

3.3io-bound与cpu-bound又该注意什么问题呢?

4.死锁!一个稍不注意就会出现的问题(ConfigAwait使用)

5.总结:


1.基础概念--进程、线程

当你编写的程序启动时,系统会在内存中创建一个新的进程。所谓进程,就是程序运行时候的各种资源的集合,这些资源有“虚地址空间”、“文件句柄”、“程序运行所需要的其他许多内容”。

在进程的内部,系统会创建一个叫线程的内核对象,他代表了真正要执行的程序,也就是说线程才是系统为处理器执行所调用的单元,而不是进程。在C#程序中,进程一旦建立,在main方法的第一行语句处就会开始线程的执行。

进程和线程都是操作系统级别的, 由系统分配出的独立单位. 但对于协程这种就是通过编译器级别完成的, 没有多分配出独立单位, 仅仅是async/await异步控制的流, 没有多个线程的切换或者锁机制。具体的协程内容并不在本文讨论范围内,如果你有兴趣,可以看看这个博主写的文章:协程的概念,为什么要用协程,以及协程的使用_someone丶的博客-CSDN博客_为什么要有协程什么是协程协程, 又称为微线程,是一种用户态的轻量级线程。协程不像线程和进程那样,需要进行系统内核上的上下文切换,协程的上下文切换是由开发人员决定的。协程是一种用户级的轻量级线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,...https://blog.csdn.net/weixin_44575037/article/details/105513014

2.异步与多线程啥关系

首先需要强调,异步和多线程不是一个级别的东西,举个不恰当的例子,就像你问我:“放假出去玩,是去北京好,还是做飞机好”。

能和"异步"对比的是"同步"。简单理解, "同步"是指一个进程发起请求之后, 需要一直等待返回信息【会阻塞你当前线程】, 而"异步"是指进程发起请求之后, 可以处理其他的事情, 等到信息返回之后再次通知这个进程处理【不会阻塞你当前线程】。

知乎“愚抄”博主举的例子我认为特别棒: 假设你有一个网站, 需要嵌套一个广告, 如果是"同步"的, 那这个广告的加载缓慢就会使得请求一直等待, "阻塞"正常页面的显示, 而如果是"异步"的, 就可以向广告服务器发起请求后, 直接开始显示其他页面, 直到广告服务器返回结果后再开始显示广告. 那我们要怎么让这个请求变成"异步"的呢? 就可以使用"多线程"这种手段。

换一个生活场景理解的话, 就比如你要通知一群人"出来吃饭吧", 同步就是一个一个打电话, 打完一个才能打下一个, 一旦其中某人和你多说了几句, 就可能影响到你后续的通知时间. 而异步就相当于你可以不被阻塞的通知到所有人, 那就有很多方式, 比如群发短信等他们回消息, 比如打一个多方通话, 比如雇很多人帮你打电话等等。

特别的一点是, "多线程"并不代表一定不会"阻塞",而实现"非阻塞"也并不一定需要"多线程"(比如node.js就是单线程且异步非阻塞)。

总结一下:

异步是最终目的,多线程只是实现异步的一种手段。对于具有大量I/O操作和不同计算的大规模应用程序,使用多线程的异步方式更有利于充分利用计算资源,并且能够照顾到非阻塞函数。这也是目前所有操作系统所采用的线程模型。所以下文我说的异步,如果没有特别强调都是指多线程!

3.C#中的async与await的使用

C#和javascript等语言都有async与await特性,使用这个特性你可以像编写同步代码一样,方便的去编写异步代码(有效避免“回调地狱”的出现,调试上、逻辑上也更清楚)。

async与await具体使用方式请看微软文档:

C# 中的异步编程 | Microsoft Docshttps://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/

3.1异步能缩短数据处理时间么?

答案:不能!

原因:同步处理和异步多线程处理,本质都是用一个线程在处理,速度上不可能有太大差别,基本一样

这里我的程序通过访问若干网站,获取其html来模拟耗时操作!分别对别同步与异步方式!

获取html的网址:

public class WebSite
    {
        public static readonly IEnumerable WebSites = new string[] {
            "https://www.zhihu.com",
            "https://www.baidu.com",
            "https://www.weibo.com",
            "https://www.stackoverflow.com",
            "https://docs.microsoft.com",
            "https://docs.microsoft.com/aspnet/core",
            "https://docs.microsoft.com/azure",
            "https://docs.microsoft.com/azure/devops",
            "https://docs.microsoft.com/dotnet",
            "https://docs.microsoft.com/dynamics365",
            "https://docs.microsoft.com/education",
            "https://docs.microsoft.com/enterprise-mobility-security",
            "https://www.bilibili.com",
            "https://blog.csdn.net",
            "https://cn.bing.com/",
            "https://docs.microsoft.com/microsoft-365",
            "https://docs.microsoft.com/office",
            "https://docs.microsoft.com/powershell",
            "https://docs.microsoft.com/sql",
            "https://docs.microsoft.com/surface",
            "https://docs.microsoft.com/system-center",
            "https://docs.microsoft.com/visualstudio",
            "https://docs.microsoft.com/windows",
        };
    }

 测试主界面:韦_恩带你用好async/await异步多线程(C#5.0引入的特性)_第1张图片

 ①同步测试

“同步获取数据”按钮事件及所用到的方法!

  /// 
        /// 同步获取数据事件
        /// 
        /// 
        /// 
        private void btnGetSync_Click(object sender, EventArgs e)
        {
            this.txtResult.Text = "";
            //精准计时器
            var stopwatch = Stopwatch.StartNew();

            //下载并展示
            DownloadWebsitesSync();

            //最后展示总耗时
            this.txtResult.Text += $"总耗时: {stopwatch.Elapsed}{Environment.NewLine}";
        }
      //同步下载方法
        private void DownloadWebsitesSync()
        {
            foreach (var site in WebSite.WebSites)
            {
                var result = DownloadWebSiteSync(site);
                ReportResult(result);
            }
        }
 //同步下载的方法httpclient
        private string DownloadWebSiteSync(string url)
        {
            var response = httpClient.GetAsync(url).GetAwaiter().GetResult();
            var responsePayloadBytes = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();

            return $"Finish downloding data from {url}. Total bytes returned {responsePayloadBytes.Length}. {Environment.NewLine}";
        }
 //展示下载的结果
        private void ReportResult(string result)
        {
            this.txtResult.Text += result;
        }

 同步测试结果:

韦_恩带你用好async/await异步多线程(C#5.0引入的特性)_第2张图片

 当前同步请求多次最短用时8.9秒,并且在请求过程中我尝试用鼠标拖动界面移动,可是界面不会有任何响应!这就是“阻塞”。因为C#的ui界面是单独一个ui线程进行管理的,而你请求数据用的就是这个线程,当请求数据的任务没完成的时侯,ui线程处于一直被占用的状态,自然你也就无法拖动界面,或者进行其他ui操作了!

②异步测试

“异步获取数据”按钮事件及相关方法

/// 
        /// 异步获取数据事件
        /// 
        /// 
        /// 
        private async void btnGetAsync_Click(object sender, EventArgs e)
        {
            this.txtResult.Text = "";
            //精准计时器
            var stopwatch = Stopwatch.StartNew();

            //下载并展示
            await DownloadWebsitesAsync();

            //最后展示总耗时
            this.txtResult.Text += $"总耗时: {stopwatch.Elapsed}{Environment.NewLine}";
        }
  //异步下载方法
        private async Task DownloadWebsitesAsync()
        {
            foreach (var site in WebSite.WebSites)
            {
                var result = await Task.Run(() => DownloadWebSiteSync(site));
                ReportResult(result);
            }
        }

注意:

同步改异步,只需要进行细微的改动,方法头上加async标识,在你认为耗时且可能阻塞ui线程的操作前面加上await就可以了,这里建议,异步方法的返回类型要是Task或者Task!

 至于DownloadWebSiteSync与ReportResult方法同上。

异步获取测试结果:

韦_恩带你用好async/await异步多线程(C#5.0引入的特性)_第3张图片

异步请求数据多次下来最快也在8.6秒上下,这也能看出来,异步请求数据并不能提高效率,但是明显的好处是我相信大家也发现了,ui线程并没有被阻塞,我可以正常拖动界面,甚至可以进行其他的界面操作!

3.2如何提高效率、更好的利用计算机资源进行数据处理呢?(并行)

那这个时候就会有小伙伴问了,那怎么才能做到即是异步访问数据又能更好的利用计算机资源提高效率呢?

答案:并行处理(并发处理)

原理就是,你通过ui线程访问数据或者另外开辟一个线程访问数据,本质上还是利用一个线程对数据进行访问,区别就是ui线程本身是否被占用!换个想法:我能不能多开几个线程一起请求数据呢?既然是异步,开一个线程也是开,那我为什么不多开几个同时对数据进行请求呢?对了,这就是并行下载了!

只需要对原来DownloadWebsitesAsync方法进行细微修改就好了,看代码:

 //异步+并发下载
        private async Task DownloadWebsitesAsync_Parallel()
        {
            List> DownWebSites = new List>();
            foreach (var site in WebSite.WebSites)
            {
                DownWebSites.Add(Task.Run(()=>DownloadWebSiteSync(site)));
            }
            //充分利用每隔线程进行数据的下载
            var results =await Task.WhenAll(DownWebSites);
            foreach (var result in results)
            {
                ReportResult(result);
            }
        }

 解释:通过循环,每一个网址都开一个线程(充分利用当前计算机性能),并将这些线程放入集合中,当所有的请求都返回的时候,我再进行展示!这种方式效果如何请看下图:

韦_恩带你用好async/await异步多线程(C#5.0引入的特性)_第4张图片

 可以发现,异步请求数据的时间几次下来基本都稳定在1.5-2.1秒之间,还记得之前多少秒么?8秒!速度上至少提高了4倍!世上就没有十全十美的事,速度上去了,但是计算机耗费的资源相比此前也增加了!可以理解为以空间换时间,当然现在的计算机性能这块还是支持你这么玩的!

3.3io-bound与cpu-bound又该注意什么问题呢?

不知拿到源码的你,在几次测试之后,有没有发现一个问题,就是每当程序第一次启动时,获取数据的时间总是很长(10秒左右),之后随着获取数据的点击次数的增多,时间也基本稳定了。

这又是咋回事呢?

答案:冷启动,因为在初始异步开辟新线程的时候,编译器会花一定的时间对线程进行初始化,这个过程会耗费时间,这就是第一次耗时长的原因!

还记得前面说的么?

韦_恩带你用好async/await异步多线程(C#5.0引入的特性)_第5张图片

 现在操作系统在处理数据请求的时候,就已经是异步方式的了(线程池也早就初始化完毕了),当任务完成后,会自动返回一个标记给主线程。简单地说,你没有必要为了请求数据单独开辟一个线程进行等待(这很浪费,因为操作系统已经这么干了!),你自己另开辟线程的时候编译器初始化又会耗费一定的时间。并且在.Net4.5以后,微软也已经发布了很多异步方法,按照规定,他们的名称后面都是以async结尾的!这些方法你用就好了,也不存在初始化问题!

明白了原理。就可以修改代码了:

修改DownloadWebSiteSync为DownloadWebSiteAsync,改为异步下载:

//异步下载的方法httpclient
        private async Task DownloadWebSiteAsync(string url)
        {
            var response =await httpClient.GetAsync(url);
            var responsePayloadBytes =await response.Content.ReadAsByteArrayAsync();

            return $"Finish downloding data from {url}. Total bytes returned {responsePayloadBytes.Length}. {Environment.NewLine}";
        }

 直接用await代替GetAwaiter().GetResult();

DownloadWebsitesAsync_Parallel方法对并行下载线程开启也不要再次task.run了:

 //异步+并发下载
        private async Task DownloadWebsitesAsync_Parallel()
        {
            List> DownWebSites = new List>();
            foreach (var site in WebSite.WebSites)
            {
                //直接引用,不用task.run新开线程进行数据等待
                DownWebSites.Add(DownloadWebSiteAsync(site));
            }
            //充分利用每隔线程进行数据的下载
            var results =await Task.WhenAll(DownWebSites);
            foreach (var result in results)
            {
                ReportResult(result);
            }
        }

再次测试看下第一次启动的效果把:

韦_恩带你用好async/await异步多线程(C#5.0引入的特性)_第6张图片

 第一次启动就能达到2秒了!

总结区分:

当你的异步需求主要是等待任务,这种就是io-bound模式,此时你不应该开辟一个线程进行等待!

但是你的异步需求主要是用来计算大量的数据,这种就是cpu-bound的模式,此时为了提高效率,才应该考虑Task.run新开辟线程!

4.死锁!一个稍不注意就会出现的问题(ConfigAwait使用)

在使用异步多线程的时候,稍微不注意就会造成死锁!这个地方我也被坑过无数次!!!...............

举个典型例子吧!

将刚 DownloadWebsitesAsync_Parallel方法的await去掉,后面加上.Result或者.GetAwaiter().GetResult(),将ui线程阻塞在这里,并不及时释放ui线程。

韦_恩带你用好async/await异步多线程(C#5.0引入的特性)_第7张图片

再次运行程序你会发现程序“die”了,点击“异步获取数据”按钮,程序卡住一动不动,且没有任何结果,无法退出!

韦_恩带你用好async/await异步多线程(C#5.0引入的特性)_第8张图片

 这就是经典的死锁现象。发生这个现象的原因是什么呢?看图:

韦_恩带你用好async/await异步多线程(C#5.0引入的特性)_第9张图片

 主线程在等待异步返回结果,异步算完后却等待返回主线程。大家互相等待,所以程序就无法进行运行了!

如何解决呢?

①在ui主线程前面加上await,让他即时释放掉主线程,不要阻塞在哪里!

使用ConfigAwait(),这个方法当参数为true时,表示异步完成后返回主线程,当参数为false时,表示异步完成后,将自己找一个空闲的线程返回,不一定返回主线程!

在DownloadWebSiteAsync方法中修改如下:

韦_恩带你用好async/await异步多线程(C#5.0引入的特性)_第10张图片

再次启动程序,你会发现,死锁现象没有了。原因也很简单,就是因为异步多线程处理完毕后发现主线程忙碌,就返回别的线程了(不忙的),ui主线程发现异步返回结果了,就继续执行了!

5.总结:

有关async与await的使用后续我将持续学习、更新,如果你有你的想法,可以评论区留言,有关本问的原代码地址,我会放在评论区!

你可能感兴趣的:(C#(.NET)总结,C#,async,await,多线程,异步)