前置说明
任务
: 做一件事任务块
: 做这件事情的一个步骤
例如:
任务
: 在控制台打印 Node.js
的 Wiki
任务块1 | 任务块2 | 任务块3 |
---|---|---|
构建 Wiki 地址(https://en.wikipedia.org/wiki/Node.js) | 向这个地址发起网络调用 | 在控制台打印返回的内容 |
我对 异步 的认识
良好的异步程序设计
- 尽可能地将与计算无关的
任务块
(如 上边例子中的任务块2
)托付给运行时 - 避免这些
任务块
对当前线程的阻塞 - 使得当前线程可以执行其它
任务
的任务块
- 从而达到并发的目的
上边的认识真的完整吗?
将与计算无关的 任务块
托付给了谁, 它为什么可以承受?
- 运行时将这些任务块托付给了操作系统, 操作系统将它们托付给了硬件, 而硬件执行自己的工作不占用 CPU 时间
- 操作系统不会来回去问硬件,“你准没准备好” “你准没准备好” “你准没准备好”...., 而是硬件在准备好的时候会给操作系统一个信号
- 在等待信号的这段时间内, 这个线程对 CPU 的占用率甚至可以达到0, 并且这个占用率并不随着它等待的事情的增加而明显增加
- 可以说, 线程在托付
任务块
之后就忘了它曾经把一个任务块
托付出去这一情况了 - 操作系统把内容准备好之后会通知
运行时
,运行时
把内容交给需求这个内容的任务
的 第一个任务块
(为什么这么说 可以看下面对 调用者 的解读 最开始的任务块(调用异步函数的函数)如果不阻塞调用根本不会收到提醒, 它甚至都不在了(执行完毕 弹出调用栈)) 任务块
在拿到内容之后进行进一步的处理 (如 上边例子中的任务块3
)
所以最后这个问题落脚点成为 硬件为什么可以承受? 看完下面的内容之后我们再说
怎样理解阻塞非阻塞与同步异步的区别?
举一个网络上的例子:
老张爱喝茶,废话不说,煮开水。出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
- 老张把水壶放到火上,立等水开。(同步阻塞) 老张觉得自己有点傻
- 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞(其实是部分阻塞, 在去厨房看的过程中是阻塞的)) 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。
- 老张把响水壶放到火上,立等水开。(异步阻塞) 老张觉得这样傻等意义不大
- 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)
老张觉得自己聪明了。
所谓同步异步,只是对于水壶而言:
- 普通水壶,同步;
- 响水壶,异步。
虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。
同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言:
- 站在水壶旁边等的老张,阻塞;
- 看电视的老张,非阻塞。
情况1和情况3中老张就是阻塞的。
虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
网上的例子常说的通知调用者,这个调用者指的是谁?
网上的例子常说(比如上面烧水的例子), 系统在准备好内容之后通知调用者。
如果我们没有在调用异步函数之后阻塞的获取值(如 Future.get ), 则那个(调用异步函数的)函数调用完异步函数之后就执行其他的事情去了, 把其他事情做完这个函数就从 调用栈
中被弹出,它根本不会关心这个异步函数成功拿到值之后怎么办,异常怎么办。
所以这个 调用者 指的是(调用异步函数的)函数所在的运行时
异步函数是一个把
- 从 IO 拿到值的工作
- 拿到值之后来的处理步骤(函数)
- 异常处理机制(函数)
这三者封装在一起的闭包
把闭包交给运行时去做, 运行时把 从 IO 拿到值的工作
托付系统,同时把
- 拿到值之后来的处理步骤(函数)
- 异常处理机制(函数)
这两个步骤保存下来
如果系统成功的返回了值 就把值交给第一个函数,否则执行第二个
既然后续的运行机制都交代清楚了, (调用异步函数的)函数就可以安心的离开了 :)
总结
正如 异步阻塞 的情况中所描述的, 我们的硬件其实完全可以承受住大量的工作。但是我们原先的做法是创建多个线程, 这些线程在等待硬件返回内容之前一直存在,导致大量的线程上下文切换耗尽了 CPU
。(使用阻塞函数所带来的)性能瓶颈, 从来不在硬件上。
系统底层不会阻塞,内核都是异步的。只不过,我们却因为习惯使用同步函数,选择像老张一样站在一个响水壶的旁边。
其实,是先有异步,同步是后封装的啊