Obeta

Async/Await会阻塞NodeJS进程吗?

我们都知道关于 NodeJS 中的一个常识是 NodeJS 是一个单线程的 JS 引擎,这是由于使用的 Chrome 的 V8 引擎造成的,而 V8 引擎为何这么设计是有多方面的原因的:

  1. 防止网站执行多个脚本影响用户体验(你用的多了,别人就没法用了)
  2. 防止多个进程操作同一个 DOM,发生资源竞争的情况
  3. JS 的设计一开始就是个玩具...

而为了弥补这种缺陷,使之能执行异步代码,因此 NodeJS 使用了一种事件循环的方式,此处可以看一下我之前写的JavaScript 事件循环(EventLoop)这篇文章,解释的很详细了。

Hack

在 ES5,ES6 的时候,NodeJS 执行异步代码还得需要使用第三方的库进行支持,比如之前的前端大神tj所写的co流程控制器,它使用了 JS 中的生成器来执行异步代码,不会阻塞 NodeJS:

var co = require('co');
var thunkify = require('thunkify');
var request = require('request');
var get = thunkify(request.get);

co(function*() {
	var a = yield get('http://google.com');
	var b = yield get('http://yahoo.com');
	var c = yield get('http://cloudup.com');
	console.log(a[0].statusCode);
	console.log(b[0].statusCode);
	console.log(c[0].statusCode);
})();

v4 之后支持了 Promise,为的是给async/await让路

var co = require('co');

co(function*() {
	// resolve multiple promises in parallel
	var a = yield Promise.resolve(1);
	var b = yield Promise.resolve(2);
	var c = yield Promise.resolve(3);
	console.log([a, b, c]);
	// => [1, 2, 3]
}).catch(onerror);

上面的代码主要的作用是顺序执行a,b,c的异步,如果不使用此类库,你就可能会掉入回调地狱里去了:

runA(function() {
	runB(function() {
		runC();
	});
});

如果你的这些a,b,c没有依赖顺序,你可以并行执行他们,co也是支持的:

co(function*() {
	var a = size('package.json');
	var b = size('Readme.md');
	var c = size('Makefile');

	return yield [a, b, c];
})();
// v4之后
co(function*() {
	// resolve multiple promises in parallel
	var a = Promise.resolve(1);
	var b = Promise.resolve(2);
	var c = Promise.resolve(3);
	var res = yield [a, b, c];
	console.log(res);
	// => [1, 2, 3]
}).catch(onerror);

但是后面随着 NodeJS 增加了Promise以及async/await语法糖后,这些库就被慢慢的冷藏了,一直没有再更新过。

Async

历史回顾完了,要回答 title 提的问题,我们需要首先知道如何写出一个异步代码,前提只能使用Promise

runA().then(() => {
	runB().then(() => {
		runC().then(() => {
			console.log('finish');
		});
	});
});

可以看出来即使是使用了Promise,写出来的代码还是会陷入到类似回调地狱的嵌套里面去,因此co库在Promise推出来后热度并没有降低多少,直到async/await的出现:

async function task() {
	await runA();
	await runB();
	await runC();
}

可以看到借鉴了.Net的异步关键字后写法一下子清晰明了起来,但是你就会开始疑惑了,如果我在 NodeJS 中使用 async 会阻塞我的服务器吗?比如:

const express = require('express');
const app = new express();

function sleep(ms) {
	return new Promise(resolve => setTimeout(resolve, ms));
}

app.get('/', async (req, res) => {
	await sleep(4000);
	res.send('sleep done');
});

由于 NodeJS 是单线程的,因此你会担心上面的代码引起服务器的阻塞,导致后一个请求需要等待前一个请求成功后才会响应。

其实并不会的,因为本质上async/await就是一个Promise,因此上面的代码其实可以转化为:

const express = require('express');
const app = new express();

app.get('/', (req, res) => {
	sleep(4000).then(() => {
		res.send('sleep done');
	});
});

这样就一眼能看懂了,那么什么情况下会阻塞 NodeJS 的进程呢?

  1. for, while这种循环
  2. JSON 的序列号与反序列化,比如JSON.stringify, JSON.parse方法
  3. 没优化过的正则表达式,比如一些'aaaaaaaaaab'.match(/^(a|a)+$/),这种正则表达式会导致匹配回溯,非常的消耗 CPU,导致 NodeJS 被阻塞(类似 ReDos 攻击)

那不会被阻塞的操作有哪些呢?

  1. 数据库请求
  2. 网络请求
  3. 文件系统访问

所以结论就是需要根据你的代码逻辑来进行判断,没有放之四海而皆准的答案。

引用

  1. will-async-await-block-a-thread-node-js

个人随笔记录,内容不保证完全正确,若需要转载,请注明作者和出处.