这四个概念是非常容易把人绕蒙的。目前我的工作环境中,已经很少有用阻塞、非阻塞的概念了。经常说的是:这是一个同步接口,这是一个异步接口。
为了说清楚这四个概念,我们先建立一个模型。甲方(调用方)、乙方(工作方)和任务(工作)本身。
甲方需要执行一个任务,他可以自己执行(同步),也可以布置任务给别人(异步)。在程序中,只有乙方对应的概念可能是一个事件循环或者另一个线程,这个由调度器决定。如果这个任务相对来说非常耗时,那么我们可以形容这个任务是阻塞执行或者非阻塞执行。阻塞是说:这个任务耗时也会完成。非阻塞是说,这个任务如果完不成我就立刻返回,并告诉执行者。
在这个模型之下,甲方 和 任务 的对应到程序中的实体都是一个函数。这是造成迷惑的根本原因。
总的来说:
阻塞与非阻塞,是用来描述工作是否特别耗时的问题。
同步与异步,是用来描述甲方如何安排任务执行的。
下面用具体的程序来表示一下。
假设有一个工作,用 read()
来表示
read();
另外有一个需要执行该任务被执行的甲方,用 process
来表示
看,甲方的外在表现形式是函数吧。
为了说清楚同步异步的概念,我们假设它执行在线程A。
void process() { // 执行在线程 A// 需要 read 被执行
}
同步意味着,工作甲方会自己做,不再外包了。所以 read
仍然会执行在线程 A ,所以 process
和 task
是一个顺序的执行流。
- process 开始 -> task 开始 -> task 结束 -> process 结束
void process() {task();
}
异步意味着,该工作我不会立刻做,可能会让别的 线程(乙方) 做,也可能我后续会做(事件循环),这由调度器来决定,这代表 process 和 task 是未知的执行流。我们能知道的是 task 只会在 postTask 之后才会执行。
- process 开始 -> postTask -> porcess 结束 -> 2. 事件循环调度到 task
1. 线程 B 立即执行 task
void process_async() {schedule.addTask(read);
}
我们可以说使用 schedule
的 process
实现,是一个 异步接口。我相信到现在为止,对异步接口的解释仍然不尽如人意,因为还有回调的问题没有解决。后面会继续深入。
现在,我们加上 阻塞和异步,对这四个概念做一个简单总结。
只有 任务
比较慢时,才会用 阻塞与非阻塞 来形容这个任务。一般是需要 io 甚至需要等很久。
- 如果
task
说我一定会完成,完不成我不回来,那么它是阻塞的。 - 如果
task
我不一定会完成,如果我无法完成,我会返回来告诉你,不会让你等很久的。那么它是非阻塞的。
注意:如果是阻塞接口,那么它一定会阻塞某个线程的执行,这无法避免。
总结表格如下:
- | 阻塞 | 非阻塞 |
---|---|---|
同步 | 当前线程等很久 | 任务可能没完成 |
异步 | 某一个线程会等很久 | 未来任务可能没完成 |
我们继续讨论异步接口的回调问题。
如果考虑到回调函数,process 会变成这样
void process_async(callback) {schedule.addTask([]() {res = read();callback(res);});
}process_async([](res) {do_something(res);
});
这是一个非常裸的异步框架,也能说明很多问题。比如说: schedule
会把 任务安排在任何一个线程,所以潜在的 callback 应该也是运行在别的线程的,这就有潜在的线程安全问题了。
每一个高级的异步编程模型,都可以用这个裸的模型来解释。所以在进行异步编程时,可以用这个裸的模型来分析潜在的问题。
我们对应 javascript 中的异步编程模型,看看怎么用这个裸的模型来解释。
第一步,使用 promise,优雅的将任务函数和回调函数合二为一,更重要的是,回调函数的相对位置在异步函数之后,这为顺序写异步代码提供了基础。
new Promise((resolve, reject) => { // 原 process_async 函数 x = read();resolve(x);}
).then((res) => { // 原 callbackdo_something(res);
})
你会说,这也没有 schedule 呀?确实没有。事实上如果 read() 是阻塞调用的话那可以说这个异步,异步了个寂寞,还是会阻塞。前面说过,阻塞任务必然会阻塞某一个线程的。由于 js 是单线程模型,那阻塞的当然是本线程啦。
可以观察一下产品中的代码,Promise 中的 resolve
调用一定实在内部函数中的,例如经典的: resolve 函数就放在 setTimeout
中,这是运行时提供的能力。运行时的事件循环提供了300ms
之后再执行 resolve
的能力。如果没有事件循环提供异步能力,可以说 javascript 的 Promise 基本就是个摆设。
const myPromise = new Promise((resolve, reject) => {setTimeout(() => {resolve("foo");}, 300);
});myPromise.then(handleFulfilledA, handleRejectedA).then(handleFulfilledB, handleRejectedB)
javascript 与非阻塞调用是密不可分的。永远不可以在 javascript中使用阻塞接口。因此 node 底层是 libuv 库,提供了事件循环和非阻塞+io 多路复用的 io 模型。
此外,do_somthing 和 handleFulfilledA 依然可以是异步的,那便是 javascript 的链式调用了。
第二步,使用 async / await 顺序写异步代码
async main() {let res = await new Promise((resolve, reject) => {setTimeout(() => {resolve("foo");}, 300);});res = handleFulfiledA(res); // handleRejectedB 可以套异常来实现res = handleFulfiledB(res);
}
总的来说,javascript 使用 promise + async/await 语法(糖?) + libuv 运行时的事件循环提供的非阻塞io接口,实现了比较优雅的异步编程模型
如果想了解更多 javascript 的 generator、async、promise 机制,可以参考这篇文章