这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战
下面的内容是对 深入浅出 nodejs 这本书的第四章 - 异步编程的学习笔记和练习
这个系列,上一篇文章讲的是函数式编程,链接:juejin.cn/post/699430…
正文开始,之前很多年之内,为了照顾编程开发人员的阅读思维习惯,同步I/O都很盛行。
在解决CPU密集计算导致的性能问题时,一般会采用多线程的方式,但是从操作系统调度多线程的上下文切换开销,到实际编程中的锁、同步等问题,也比较麻烦。还有一种方式时通过 c/c++ 调用底层操作系统接口,手动完成异步 I/O,这种方式的性能很好,但是门槛比较高。
nodejs 将异步 I/O 直接提到语言面,可以在业务中直接使用,是一种创新。
异步编程的优势
nodejs 是基于事件驱动的非阻塞 I/O 模型。通过事件循环、异步I/O,单线程的 nodejs 明显擅长处理 I/O 密集问题,对于 CPU 密集问题,需要看一下单线程的处理能力。
通过斐波那契数列的计算结果来看,C 语言的处理能力最强,拥有 V8 引擎(V8 引擎把 JavaScript 代码编译成更快的机器语言,可以被计算机直接执行)的 nodejs 处理能力稍弱一点,但是如果编写 node addon(C/C++ 扩展模块),node 也可以和 C 媲美。
编程时需要注意不要让任何一个计算耗费过多的 CPU 时间片,建议对 CPU 的耗用不要超过 10ms,可以将大量计算分解成小的计算,通过 setImmdiate 进行调度。这样不论是 CPU 密集型,还是 I/O 密集型,都是没有问题的。
异步编程的难点
异常处理
在编写异步函数时,比如下面的例子,因为 callback 是在微任务中,是在当前事件循环完成后执行的。
var asyncFunction = function (callback) {
process.nextTick(callback);
};
于是单纯按照一般的写法,在外面包一层 try...catch 是不能 catch 住的,能 catch 住的是当前事件循环内的异常。错误的写法比如:
try {
asyncFunction(callback);
} catch (e) {
// TODO
}
在异步函数中,nodejs 处理异常有一个约定,是把 err 作为第一个实参返回,在使用 asyncFunction 时,接收的第一个实参是 err:
asyncFunction((err, result) => {
// TODO
})
我们去编写异步函数(比如:asyncFunction)时,要注意:必须执行调用者传入的回调函数;正确返回异常供调用者判断。
asyncFunction 的实现,比如:
var asyncFunction = function (callback) {
try {
const result = JSON.parse(buf);
} catch (err) {
err.body = buf;
err.status = 400;
return callback(err);
}
callback(null, result)
};
函数嵌套过深
异步I/O的编程方式,可能会出现多个异步调用嵌套的问题,比如从目录中读取文件:
fs.readdir(path.join(__dirname, '..'), function (err, files) {
files.forEach(function (filename, index) {
fs.readFile(filename, 'utf8', function (err, file) {
// TODO
});
});
});
这种互相依赖的嵌套或许情有可原,但是在互相没有依赖的情况下,尽量避免这种写法
阻塞代码
如果需要实现 sleep(1000) 这种让线程沉睡的效果,不要写循环长时间占用单线程CPU,尽量使用 setTimeout 实现。
多线程编程
相对于浏览器的 web workers,多线程编程可以使用 child_process 基础 API,和 cluster 模块做更深层的应用
异步转同步
nodejs 的同步编程 API 会比较少,可以修改为异步或者借助库或编译的手段来实现
以上是 异步编程的优势与难点 的介绍,欢迎评论和点赞~