JavaScript异步函数的开销是什么?

25
问题: 在引擎运行时将函数声明为async并最终await,与常规函数的返回语句相比,是否存在计算开销(如果有,程度如何)?
async function foo() {
    var x = await bar(); // <--- bar() is non-blocking so await to get the return value
    return x; // the return value is wrapped in a Promise because of async
}

对比

function foo() {
    var x = bar(); // <--- bar() is blocking inside its body so we get the return value
    return new Promise(resolve => { resolve(x); }); // return a Promise manually
}

上下文:

由于Javascript(例如Nodejs)采取了异步方向,为什么他们不考虑每个函数默认都是异步的(如async关键字)?

这样,人们可以决定将任何函数调用视为一个Promise并玩异步游戏,或者只await必要的部分。

我想,在函数体内await会创建本地函数作用域的开销,而当函数返回时,正常的事件循环会继续进行,并且无需将内部函数作用域推入堆栈中?

这归结为一个奖励问题:在需要一次同步IO操作(请参阅注释)的复杂类层次结构中(某处深处),如果该方法标记为async,则仅在标记为async的调用函数能够再次await。因此,所有标记为asyncawait都需要处理这种情况吗?

注意:请不要争论不执行任何同步操作的必要性,因为这不是重点。

注意2:本问题不涉及awaitasync的含义或执行时间。本问题关注的是性能和语言内部机制(即使存在多个实现,概念可能存在固有的语义开销)。


{btsdaf} - Andy Ray
2
{btsdaf} - Simon
1
{btsdaf} - Felix Kling
bar()在其函数体内是阻塞的 - 这样是行不通的。即使是async function也是异步的 - 非阻塞的,这就是Node.js并发的全部意义所在。 - Bergi
{btsdaf} - Bergi
{btsdaf} - Bergi
3个回答

23

异步函数与同步函数相比具有固有的开销。虽然可以将所有内容都设为异步,但很可能会很快遇到性能问题。

同步 vs 异步

一个函数返回一个值。

一个异步函数创建一个Promise对象来从函数中返回。Promise对象被设置为维护异步任务的状态并处理错误或后续的链接调用。承诺将在事件循环的下一个刻度中解决或拒绝。(如果您想要详细信息,请阅读规范)这相对于简单的函数调用和返回值具有内存和处理开销。

尽管量化开销有点无用,因为大多数异步函数是由于它们必须等待外部Node.js线程完成一些工作而异步的,通常是做缓慢的IO。与整个操作的时间相比,建立Promise的开销相当小,特别是如果替代方案是阻塞主JS线程。

另一方面,同步代码在主JS线程中立即运行。交叉区域是调度同步代码,无论是为了计时还是为了“节流”使用主JS线程,以便GC和其他异步任务有机会运行。

如果您在紧密的循环中逐个字符解析字符串,则可能不希望在每次迭代中创建一个Promise并等待其解决,因为完成该过程所需的内存和时间要求将很快爆炸。

另一方面,如果你的应用程序只是查询数据库并将结果转储到koa http响应中,那么你很可能会在异步promise中完成大部分操作(尽管在底层仍然有许多同步函数使其发生)。

愚蠢的例子

对一个人为制造的例子进行基准测试,比较同步返回和各种异步方法来解决相同的同步操作之间的差异。

const Benchmark = require('benchmark')
const Bluebird = require('bluebird')

let a = 3

const asyncFn = async function asyncFn(){
  a = 3
  return a+2
}

const cb = function(cb){
  cb(null, true)
}
let suite = new Benchmark.Suite()
suite
  .add('fn', function() {
    a = 3
    return a+2
  })
  .add('cb', {
    defer: true,
    fn: function(deferred) {
      process.nextTick(()=> deferred.resolve(a+2))
    }
  })
  .add('async', {
    defer: true,
    fn: async function(deferred) {
      let res = await asyncFn()
      deferred.resolve(res)
    }
  }) 
  .add('promise', {
    defer: true,
    fn: function(deferred) {
      a = 3
      return Promise.resolve(a+2).then(res => deferred.resolve(res))
    }
  })
  .add('bluebird', {
    defer: true,
    fn: function(deferred) {
      a = 3
      return Bluebird.resolve(a+2).then(res => deferred.resolve(res))
    }
  })

  // add listeners
  .on('cycle', event => console.log("%s", event.target))
  .on('complete', function(){
    console.log('Fastest is ' + this.filter('fastest').map('name'))
  })
  .on('error', error => console.error(error))
  .run({ 'async': true })

运行

→ node promise_resolve.js
fn x 138,794,227 ops/sec ±1.10% (82 runs sampled)
cb x 3,973,527 ops/sec ±0.82% (79 runs sampled)
async x 2,263,856 ops/sec ±1.16% (79 runs sampled)
promise x 2,583,417 ops/sec ±1.09% (81 runs sampled)
bluebird x 3,633,338 ops/sec ±1.40% (76 runs sampled)
Fastest is fn

如果您想更详细地比较各种Promise和Callback实现的性能/开销,请查看蓝鸟基准测试

file                                       time(ms)  memory(MB)
callbacks-baseline.js                           154       33.87
callbacks-suguru03-neo-async-waterfall.js       227       46.11
promises-bluebird-generator.js                  282       41.63
promises-bluebird.js                            363       51.83
promises-cujojs-when.js                         497       63.98
promises-then-promise.js                        534       71.50
promises-tildeio-rsvp.js                        546       83.33
promises-lvivski-davy.js                        556       92.21
promises-ecmascript6-native.js                  632       98.77
generators-tj-co.js                             648       82.54
promises-ecmascript6-asyncawait.js              725      123.58
callbacks-caolan-async-waterfall.js             749      109.32

{btsdaf} - Simon
值得注意的是,在基准测试中添加直接异步函数调用 let res = asyncFn(); deferred.resolve(res); - dy_

-1

有些操作您不想等待。例如,如果您想要执行多个XHR,在同时加载多个文件时,自动等待会使加载变得线性,这是不好的。


-1
异步/等待和Promise的美妙之处在于你可以混合使用它们。对于多个XHR请求,你可以简单地返回Promise.all():
async function fetchPages() {
    return Promise.all([
        fetch("this.html"),
        fetch("that.html")
    ]);
}

for (var page of fetchPages() { ... }

1
这与async无关,您可以在函数签名中不使用async运行相同的代码。如果您有await Promise.all(...),情况将会有所不同。 - Jack Ryan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接