async/await到底该怎么用?如何理解多线程与异步之间的关系?
前言
如标题所诉,本文主要是解决是什么,怎么用的问题,然后会说明为什么这么用。因为我发现很多萌新都会对之类的问题产生疑惑,包括我最初的我,网络上的博客大多知识零散,刚开始看相关博文的时候,就这样
。然后博文也不一定正确,又变成这样
,当然我的观点也不一定正确,所以,以免误导萌新,有疑问,欢迎提出!有错误,欢迎指正!
一、首先看几个问题
- 多线程程序比单线程程序效率高?
- 什么是IO密集型程序?计算密集型程序又是什么?
- IO密集型程序和计算密集型程序与多线程和单线程有什么关系?
- 同步、异步、阻塞、非阻塞又是啥?
- 多线程与异步到底是啥关系?
二、先了解操作系统的演变过程
在早期的计算机时代,那时候硬件宝贵,什么硬件资源都要精打细算,操作系统刚开始也非常简单,之后一步步发展,大概经过了以下阶段:
1.单道批处理系统(一次只能处理一个任务,任务排队,串行执行,无交互能力,系统资源利用率不高)
2.多道批处理系统(外存中有一个后备任务队列,一次取多个任务到内存,当任务处于IO时,会切换到其他任务,无交互能力,系统资源利用率较高)
3.分时操作系统(每个任务都能尽可能的得到调度,有交互能力,系统资源利用率比多道批处理系统略低,因为要花很多时间在调度任务上,例如Windows)
4.实时操作系统(每个任务都能实时调度,有交互能力)
下面简单的了解一下单道批处理系统和多道批处理系统。
单道批处理系统处理任务过程
![]()
可以看到在执行任务的过程中,有一部分时间被IO(比如等待用户输入,读取文件内容)占用了,而CPU无事可做,浪费系统资源,为什么IO不需要CPU
参与呢?因为有相应的IO设备,相当于是一个小型的计算机,在有了DMA(直接存储器)后,IO设备访问内存也不需要CPU参与了,大大减少了中断次数,
CPU基本上只要发出IO指令,通知IO设备工作,然后就可以做其他事情了,等待IO处理完,会发出一个中断,然后CPU接着处理未完成的任务。为了提高C
PU利用率,于是便有了多道批处理操作系统。
多道批处理系统处理任务过程
相比于单道批处理系统,可以看到,在完成T1、T2、T3任务过程中,实际只花了CPU10S,其中额外的调度时间花了1S,总共11S。CPU利用率大大提高,但是还是有一个致
命缺点——无法交互。只有在IO时或者任务已经完成的情况下,才会发生调度。这个对用户体验就非常不好了,只要其中一个程序产生死循环或者什么原因,就会导致后面
的任务无法调度,想要恢复执行,必须使用万能的“重启大法”,并且找BUG还不能实时调试,必须重启计算器再重新启动程序,这就很难受。所以分时操作系统就出
现了。
分时操作系统处理任务过程
在多道批处理系统过程中,是没有分时的这个概念的,它的唯一目的是提高CPU的利用率,不让CPU空转。但这个用户体验就非常的差了,为了提高用户
体验,换句话说就是为了让每个任务都能得到CPU的眷顾,CPU就发表了以下观点:
CPU:以前你们老是说我不照顾你们,现在我决定了,我给你们每个人固定的时间,然后轮流照顾你们,这样行不?(时间片轮转调度算法)
众任务:好好好!
……
过了许久之后,
任务A:我有急事!我有急事!呼叫呼叫!此时CPU还在服务其他任务,按照规则,任务A还要轮转999次才能到达A,于是任务A卒。
过了N久,CPU过来了,发现了这个情况,于是它又改变了规则,按照某种优先级算法,然后进行轮转,例如高优先级任务优先低优先级任务轮转等等。
这个时候,才很好的解决了用户体验差的问题。
拿上面的场景,对应现代操作系统。任务(或者说作业),可以抽象成线程。例如windows,一个基于线程优先级抢占式的分时操作系统。现在可以回答
如下几个问题的一部分。
什么是IO密集型程序?什么是计算密集型程序?
IO密集型就是指一个程序的执行时间中,IO操作占了绝大多数时间,比如Web服务器,涉及了大量IO操作,HTTP请求,数据库读取,模板渲染引擎
读取模板文件(通过缓存可以解决)等IO操作,实际要CPU参与计算的部分很少,反之就是计算密集型程序,例如视频编码输出,加密解密、科学计算等等。
多线程效率比单线程效率高?
不讨论多核或者多CPU的情况下,对于计算密集型程序来讲,单线程效率一般是最高的,因为它不需要进行线程调度,就不会产生调度开销,调度开销
包括调度算法的执行,线程上下文的切换(堆栈寄存器和相关寄存器以及程序计数器的还原)等等,你可能会问,不是还有其他线程吗?的确是有,但
一般来讲,大多数线程是处于挂起状态的,而挂起的线程是不会分到CPU时间片,就算有些许线程处于活动状态,但是基本上分配到的时间片很少(看下图),而
且由于活动线程数其实很少,所以调度开销也很小,所以单线程效率比较高,如果开多线程来执行这个计算密集型程序,情况就不一样了,因为是同等
优先级,所以会发生频繁的线程调度,产生额外开销,当计算任务很长时,这个就非常明显了,虽然对于整体时间来说不明显。
那如果是IO密集型呢???这就跟异步有关系了。
三、阻塞、非阻塞、同步、异步
这里的A和B的主体是不确定的,并且 需要注意,这里至少有两个角色
同步:A和B有顺序,A完成工作之后B才能继续工作。
异步:A和B无顺序,A和B可以同时工作
阻塞和非阻塞是有一定语境的,它是专门针对线程来说的,它指的是状态
阻塞:意思是指线程被挂起,不能做其他任务
非阻塞:意思是指线程未被挂起,处于就绪或运行状态,可以做其他任务
有常说的以下四种组合:
同步阻塞、同步非阻塞(不存在)、异步阻塞(不存在)、异步非阻塞
现在假设一个场景:
线程A在执行代码的过程中,其中执行到了一个ReadFile()函数,这个任务最后交由IO设备B完成,很明显,线程A可以继续执行其他代码,在IO设备B完成之后,线程A继续执行依赖于ReadFile()的代码块。很明显,他们之间是异步的,也就是CPU于IO设备之间是异步的,因为他们能同时工作。那么代码看起来就可能像下面这样
result = ReadFile();//首先发起异步请求,以便IO设备能尽早处理
flag = true;
if(result.IsComplied && flag){做依赖于读文件的操作();flag = false;}
吃西瓜();
if(result.IsComplied && flag){做依赖于读文件的操作();flag = false;}
打War3();
或者这样
result = ReadFile();//首先发起异步请求,以便IO设备能尽早处理
吃西瓜();
打War3();
while(!result.IsComplied);
做依赖于读文件的操作();
或者这样
ReadFile(callBack:做依赖于读文件的操作);//首先发起异步请求,以便IO设备能尽早处理
吃西瓜();
打War3();
可以看到:
在第一种模式下,写代码复杂丑陋;
在第二种模式下,代码相对于比较优雅,但可能需要轮询,忙等,浪费CPU时间。
第三种模式,好像非常好,但其实是回调层数不够深,也就是所谓的回调地狱,虽然有办法可以把嵌套式改成平坦式,例如then.then.then的形式,但是代码还是不够优雅,所以出现了async/await形式,也就是号称的用写同步代码的方式写异步代码,不理解的看下面就理解了。
为什么不够优雅?究其原因是因为它们之间是异步的,那有没有一种办法能让它们之间同步进行呢?只要IO设备没完成,线程A就不能执行代码,不能工作,待IO设备完成之后,线程继续执行代码,继续工作,那么代码看起来就像这样。
吃西瓜();
打War3();
result = ReadFileSync();
做依赖于读文件的操作();
那重点来了!!!怎么做到呢?阻塞。在执行到ReadFileSync()时,把线程阻塞。也就是说操作系统其实是用阻塞模拟同步,所以说同步代表着阻塞。也就是同步阻塞的由来。那么同步非阻塞呢?没有!按照同步定义,A完成工作之后,B才能继续工作,如果是非阻塞,那么就不叫同步了,因为A和B可以同时工作,所以非阻塞只能搭配异步。这个模式的缺点就是,会导致创建许多线程。
,在早期Web服务器中,针对每个请求,创建一个线程,请求结束之后,线程销毁,因为创建线程和销毁线程代价非常大,所以发明了线程池,虽然有了线程池,但如果线程执行了同步IO,那么还是会导致线程阻塞,从而依然会导致该线程不能及时回收利用,从而又会导致创建许多线程,所以我们要尽量写异步非阻塞代码,但是写异步非阻塞代码又不够优雅
,怎么办呢?怎么办呢?怎么办呢?这时候主角async/await闪亮登场。
吃西瓜();
打War3();
result = await ReadFileAsync();
做依赖于读文件的操作();
看见没有!!!这个和同步版的是不是差不多?和同步版达到的效果是一样的,但不会导致线程被阻塞,可以让线程及时去处理其他任务。另外,由于CPU和IO设备是异步的,所以应尽早发起异步请求,正确的做法应该是下面这样的
result = ReadFileAsync();//首先发起异步请求,以便IO设备能尽早处理
吃西瓜();
打War3();
await result ;
做依赖于读文件的操作();
那么阻塞、非阻塞、同步、异步是啥的问题也解决了。
四、async/await怎么工作?
在这里简述一下,在C#中,每个使用了await的异步方法,都会被编译器魔改成了一个状态机的实现,使用了n个await关键字,就会有n+1个状态,利用这个状态机,便可以实现异步函数的挂起和恢复(有没有感觉和线程很像?),以便异步任务完成之后,回到刚开始使用await的地方,然后继续执行。在具体一点点,每当一个线程执行await的地方,这个线程就会回到被调用的地方,如果被调用的地方也使用了await,然后继续上一步骤,最终会回到线程池,如果有其他任务就继续执行其他任务,否则会阻塞,这没关系,因为已经没事做了。等待某个时候,异步任务完成,触发状态机调用MoveNext(),然后回到调用这个await的地方,继续往下执行,需要注意的是,这时候执行线程不一定是刚开始调用await的线程了,这是由任务调度器决定的,在.NET中,默认的任务调度器使用了默认的线程池作为任务的消费者,也可以自定义任务调度器,让一个线程处理所有任务。
五、理解Task
在C#中,async/await是与Task协同工作的,而异步函数就是一个Task,Task与async/await配合可以被挂起和恢复,是不是有点线程的味道?它其实就是用户模式下的“线程”,也就是协程,线程需要调度,协程同样需要调度,不同的是,线程是抢占式的,被动的。协程需要主动让出执行权,也就是非抢占式的,通过await便可以让出执行权,所以协程可以充分利用线程资源,执行用户代码,Task便可以理解为一个协程,最后,Task最终是要由线程来执行的,可以是一个线程,也可以是多个线程,这个是由任务调度器决定的。由于默认的任务调度器使用的是线程池中的线程来执行,所以await前后执行线程很可能不一样。要想使用自定义的任务调度器,通过创建TaskFactory实例指定任务调度器,或者创建Task实例,并在Start(TaskSheduler t)传入指定的任务调度器,通过Task.Run()方法或者不传参的Start()实例方法默认都使用的是默认的任务调度器。
我写了一个单线程同步阻塞、多线程同步非阻塞、单线程异步非阻塞、多线程异步非阻塞的简单的Web服务器作为示例,并有大量注释。还有一个反编译异步方法的C#实现,和一个自定义的任务调度器,并有一个简单的发起并发Http请求的控制台程序,有兴趣的可以研究下,可以加深萌新对async/await的理解,最后还有开头的一些疑问没解决,理解这几个例子你就能知道答案了。
仓库地址:http://gitlab.fyfhk.cn/hekun3344/simplehttpserver
最后
觉得有收获的
async/await到底该怎么用?如何理解多线程与异步之间的关系?的更多相关文章
- Thread,ThreadPool,Task, 到async await 的基本使用方法和理解
很久以前的一个面试场景: 面试官:说说你对JavaScript闭包的理解吧? 我:嗯,平时都是前端工程师在写JS,我们一般只管写后端代码. 面试官:你是后端程序员啊,好吧,那问问你多线程编程的问题吧. ...
- iOS 深入理解UINavigationController 和 UIViewController 之间的关系
创建三个类 BasicViewController : UIViewController SecondViewController : UIViewController ThirdViewContro ...
- 如何理解Nginx, WSGI, Flask之间的关系
概览 之前对 Nginx,WSGI(或者 uWSGI,uwsgi),Flask(或者 Django),这几者的关系一存存在疑惑.通过查阅了些资料,总算把它们的关系理清了. 总括来说,客户端从发送一个 ...
- 深入理解理解 JavaScript 的 async/await
原文地址:https://segmentfault.com/a/1190000007535316,首先感谢原文作者对该知识的总结与分享.本文是在自己理解的基础上略作修改所写,主要为了加深对该知识点的理 ...
- async/await的实质理解
async/await关键字能帮助开发者更容易地编写异步代码.但不少开发者对于这两个关键字的使用比较困惑,不知道该怎么使用.本文就async/await的实质作简单描述,以便大家能更清楚理解. 一.a ...
- C#中async/await中的异常处理
在同步编程中,一旦出现错误就会抛出异常,我们可以使用try-catch来捕捉异常,而未被捕获的异常则会不断向上传递,形成一个简单而统一的错误处理机制.不过对于异步编程来说,异常处理一直是件麻烦的事情, ...
- 关于C#中async/await中的异常处理(上)-(转载)
在同步编程中,一旦出现错误就会抛出异常,我们可以使用try…catch来捕捉异常,而未被捕获的异常则会不断向上传递,形成一个简单而统一的错误处理机制.不过对于异步编程来说,异常处理一直是件麻烦的事情, ...
- 关于C#中async/await中的异常处理(上)
关于C#中async/await中的异常处理(上) 2012-04-11 09:15 by 老赵, 17919 visits 在同步编程中,一旦出现错误就会抛出异常,我们可以使用try…catch来捕 ...
- 浅谈Async/Await
概要 在很长一段时间里面,FE们不得不依靠回调来处理异步代码.使用回调的结果是,代码变得很纠结,不便于理解与维护,值得庆幸的是Promise带来了.then(),让代码变得井然有序,便于管理.于是我们 ...
随机推荐
- c# 优化代码的一些规则——字符串使用优化[四]
前言 在我们的程序中,经常使用到字符串,字符串的写法非常多,但是有一个问题就是我们写的字符串是否合适呢? 正文 内插符 介绍一个东西叫做内插字符,如下: static void Main(string ...
- C/C++多参数函数参数的计算顺序与压栈顺序
一.前言 今天在看Thinking in C++这本书时,书中的一个例子引起了我的注意,具体是使用了下面这句 单看这条语句的语义会发现仅仅是使用一个简单的string的substr函数将所得子串pus ...
- python 反向shell后门
linux 编码改为utf-8,windows 默认gbk,python一般都是白名单减少查杀可能性,端口可以改为443,ssl混肴数据传输. python client端 import subpro ...
- Python之TestLink篇
如何让时间变慢? 你们不知道吧,这个时候翻开书,时间又变慢了一倍,可以这样延年益寿,哈哈哈 ------------------------------------------------------ ...
- PHP获取临时文件的目录路径
PHP获得临时文件的文件目录相对路径,能够 根据tempnam()和sys_get_temp_dir()函数来完成. 下边我们运用简单的代码实例,给大伙儿介绍PHP获得临时文件的文件目录相对路径的方式 ...
- 小谢第8问:ui框架的css样式如何更改
目前有三种方法, 1.使用scss,增加样式覆盖,但是此种方法要求css的className需要与框架内的元素相一致,因此修改时候需要特别注意,一个父级的不同就可能修改失败 2.deep穿透,这种方法 ...
- 搭建Prometheus平台,你必须考虑的6个因素
作者简介 Loris Degioanni,Sysdig的创始人和CTO,同时还是容器安全工具Falco的创建者. 原文链接 https://thenewstack.io/6-things-to-con ...
- 如何从0创建一个react项目
1. 确保本机电脑安装了yarn和node: 2. 在需要安装的文件夹目录下输入:create-react-app +(项目名称): PS:上图使用的软件为webStorm 3. 此时一个简单的re ...
- linux 最基本的命令
1.说一些你比较常用linux指令 1.1.ls/ll.cd.mkdir.rm-rf.cp.mv.ps -ef | grep xxx.kill.free-m.tar -xvf file.tar.(说那 ...
- Linux 日志管理简介
查看日志rsyslogd是否启动和自启动 ps aux | grep rsyslogd 查看自启动(CentOS 7使用,CentOS 6可以使用chkconfig命令) systemctl list ...