什么是异步?
你早上起来做饭,把食材放入之后,设定好时间,如果你只是等待,没有离开做别的事,就是同步;但你不会一直等着,多半会玩手机,或者打扫之类的,等到时间到了,或者打扫完了,才去吃饭,那就是异步。
什么是异步编程?
要看异步编程,先看同步编程。什么是同步。同步,synchronous.就是按代码编写的顺序,从上往下顺序执行。
每个Task依次执行,T1执行完,T2才能执行;T3有错误了,T4就不能执行。
一般地,程序正常执行,但是遇到耗时的任务,怎么办?比如大文件上传/下载、调用第三方服务、哈希运算等。执行这些任务时,会使当前线程什么也做不了,持续长时间消耗在这个任务上,或者只是等待第三方调用返回,无法及时处理其他任务。并不能及时处理并发任务。
也就是说,后面的任务依赖于当前任务,当前任务无法完成,就不能继续往下执行。这就阻塞了当前线程。
这该怎么处理?现代处理器的多核架构,我们可以简单粗暴地就直接开一个线程,处理耗时的任务。
如上图,耗时的任务可以放到另一个线程,保证主线程不阻塞。但这也有问题:
- 多线程编程很麻烦,如果不同线程间任务有依赖,线程间通信和调度麻烦
- 一个线程遇到耗时任务就只能等待,那么计算资源就浪费了,并没有最大化利用硬件资源。
生活中,我们没有连续的一小时来玩游戏,但是你排队吃饭的时候玩10分钟,通勤的时候玩10分钟,睡前玩30分钟,60分钟的分钟都是拆成了更小的任务,一点一点完成。这就实现了异步。
异步是把任务拆分之后交替执行,而不是一个执行完再执行下一个。
显然,异步的处理,需要更多的代码来处理控制逻辑,怎样拆分任务,任务之间的依赖如何处理,等等。如果都是单线程,为什么要选异步呢?操作起来还更复杂。
再来看玩游戏的例子,为什么不等玩游戏结束了再去坐公交?因为坐公交更紧急,而游戏可以之后再玩。UI编程中,总是优先处理点击、输入这些交互事件,而不是等待文件下载,阻塞在那里。
那么异步如何实现?
现代编程有多种异步的实现方式,比如callback、Promise/Future、async/await.先来看一看callback。
什么是callback?
callback,回调。在计算机编程中,一个回调是对某一块可执行代码的引用,被引用的代码作为参数传递给另一块代码。即,call then back.
这个调用在同步回调中就立即执行,在异步回调中就在之后的某个时间执行。也称为阻塞和非阻塞。
C语言中的回调
回调函数还在函数调用的context中执行。通过函数指针来实现同步回调,将一个函数的地址传给另一个函数。
1 |
|
有没有很像OOP中的控制反转,这就是C语言的IoC实现。
但这也是同步执行的啊,回调如何实现异步执行呢?
最简单的就是定时器回调,就好比,遇到重要的事情,暂时玩不了游戏,我们会设置一个提醒,到晚上10点,玩游戏。
1 |
|
1 | execute first |
可以看到,先执行了后面的printf
语句,时间到了之后,才执行定时器回调中的printf
语句。
JS中的回调
1 | const arr = [12, 21, 22]; |
由于Chrome浏览器的多进程架构,Browser Process中有网络线程、UI线程、存储线程等。所以,JS中有些异步,是通过浏览器的多线程来实现的。
为什么要用异步编程?
换句话说,异步相比于同步,优点在哪里?
上面提到,同步容易阻塞当前线程,而异步是非阻塞的。也就是说对于经常阻塞的情况,异步是优于同步的。好比等车的时候,在地铁上,一般会掏出手机玩游戏,因为必须等,等车到站。那么,什么情况下,线程必须要等呢?等待执行I/O操作。大量的I/O操作,同步线程的执行是这样的:
CPU 的数据传输速率比磁盘或网络连接快几个数量级。因此,同步执行大量 I/O 的程序将在磁盘或网络上花费大量时间阻塞。这就是阻塞IO。异步把需要等待的任务都放到最后,总的等待时间减少了。
所以,异步在这种情况下效率更高:
任务量大,但总是有任务可以继续
任务执行了大量I/O,导致同步程序在阻塞时浪费大量时间
任务之间大部分相互独立,不需要任务间通信
这些条件几乎完美符合了C/S架构中一个繁忙的网络服务器的特点。每个task表示一个客户端请求。异步模型的应用之一就是web服务器,这也是为什么近年来Node.js越来越流行。