【8.10】nodejs 原理学习 - 异步编程(2) 异步编程的优势与难点

412 阅读3分钟

这是我参与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 会比较少,可以修改为异步或者借助库或编译的手段来实现

以上是 异步编程的优势与难点 的介绍,欢迎评论和点赞~