求高手点拨. node js 中关于同步和异步调用的反直觉现象.
发布于 4 年前 作者 Chunlin-Li 3206 次浏览 来自 问答

几乎所有提到 Node 的地方, 都强调异步的优越性, 有异步方法, 则尽量不使用同步方法. 强调同步执行的时候会将 node 线程卡住, 导致运行效率下降, 而使用异步方法可以让 node 线程继续执行其他任务, 效率更高.

但是, 请看以下代码. 只是对一段压缩后的字符串进行解压. node 的 zlib 库提供了同步和异步两种 inflate 方法. 这段代码对两种方式的性能做了一个比较. 主要考察每次解压的时间, 单位时间内完成的解压次数, 以及CPU内存开销.

代码 gist 连接

'use strict';

const zlib = require('zlib');

const orgStr = 'Node is similar in design to, and influenced by, systems like Ruby\'s Event Machine or Python\'s Twisted. Node takes the event model a bit further, it presents an event loop as a runtime construct instead of as a library. In other systems there is always a blocking call to start the event-loop. Typically behavior is defined through callbacks at the beginning of a script and at the end starts a server through a blocking call like EventMachine::run(). In Node there is no such start-the-event-loop call. Node simply enters the event loop after executing the input script. Node exits the event loop when there are no more callbacks to perform. This behavior is like browser JavaScript — the event loop is hidden from the user.HTTP is a first class citizen in Node, designed with streaming and low latency in mind. This makes Node well suited for the foundation of a web library or framework.Just because Node is designed without threads, doesn\'t mean you cannot take advantage of multiple cores in your environment. Child processes can be spawned by using our child_process.fork() API, and are designed to be easy to communicate with. Built upon that same interface is the cluster module, which allows you to share sockets between processes to enable load balancing over your cores.';

const compressed = zlib.deflateRawSync(orgStr);

var count = 0;
var timeout = 0;
var res;

function start1() {  // sync
    let start, i;
    setTimeout(start1, 0);

    for (i = 0; i < 50; i++) {

        start = process.hrtime();

        res = zlib.inflateRawSync(compressed);

        let end = process.hrtime(start)[1] / 1e6;
        if (end > 5) timeout ++;    //  time interval show below 5 ms
        count ++;
    }
}

function start2() {  // asyncs

    (function fn() {

        setTimeout(fn, 0);

        for (var i = 0; i < 50; i ++) {

            let start = process.hrtime();

            zlib.inflateRaw(compressed, function (err, buf) {

                let end = process.hrtime(start)[1] / 1e6;
                if (end > 5) timeout ++;
                count ++;
            });
        }
    })();
}

// test start1 for sync or start2 for async
start1();


setInterval(() => {
    console.log('timeout : ', timeout + '/' + count);
    timeout = 0;
    count = 0;
    // console.log('mem: ', JSON.stringify(process.memoryUsage()));
}, 1000);

以上是一个遇到的实际问题的抽象. 实际场景下对性能比较敏感. 因此, 我想知道, 为什么这种情况下异步方式看上去完败. 我到底应该使用同步方式来处理这个问题还是用异步方式?

7 回复

性能敏感就是说是计算密集了,不是IO密集。 计算密集用nodejs还是不太好吧。 计算量这块:同步比异步是要快点,因为不用等下一次 tick。 计算密集最好还是做成c++ addin 吧。

楼上的已经说了。如果程序是CPU bound的,用node没有什么意义,因为本身异步的好处就是避免CPU自身的等待,现在CPU本身就自顾无暇,异步就失去意义,反而带来不必要的编程难度;如果程序是明显的IO bound,比如网络访问,文件读写等等,node才比较有用。

@htoooth @flamingtop 非常感谢! 不过应用本身确实不是计算密集型, 而是网络IO密集的. 我说的性能敏感是指服务的并发量非常大, 并且要求响应时间尽可能的短, 希望一次压缩或解压占用的一次请求的处理时间不要超过 1% .

@htoooth 提到同步比异步快是因为不用等下一次 tick. 其实就是因为这个, 所以我的直觉是异步不用一个一个去等待响应, 因此也许相应时间稍微差一点, 但同一时间的并发量应该很高. 总的算下来单位时间处理次数应该要比同步多很多. 然而, 测试结果是反直觉的.

对于提到的 c++ addon, 首先 node 的 zlib 库本身就是 c++ 实现. 其次, 如果不考虑 zlib 的这个事. 之前我有用过一个 murmurhash 的库, C++ 实现, 并且同时提供了同步和异步两种调用方式. 当时的测试情况也是同步的响应时间和吞吐量反而优于异步.

node 的一些基本知识我还是了解一些的, 我想知道的主要是我所描述的这种现象的原因. 以及在这种场景下我使用同步调用可能会带来的风险.

异步调用解压是在另一个线程里执行的,我不确定这有多少影响,但可能是一个因素。

这样和你说吧,异步的优势在于高并发,能同时响应多个请求,因为在io的时候进程没被卡住,等待io完成,因此在同一时间处理的并发请求量大大提高

来自酷炫的 CNodeMD

你这样写再看看结果 (() => { zlib.inflateRaw(compressed, function (err, buf) {

    let end = process.hrtime(start)[1] / 1e6;
    if (end > 5) timeout ++;
    count ++;
});

})();

@flamingtop 谢谢.
@iyuq 抱歉, 不太明白你想要表达什么意思, 以及和我遇到的问题之间的关系… @jp1969 谢谢, 测试了你提供的写法, 没有明显变化. 而且似乎这只是将一个函数调用放在了另一个立即执行的匿名函数中而已, 不知道这样写执行机制上会有什么差异呢?

回到顶部