由于异步生成器中的 promise 的非并行等待而导致减速

IT技术 javascript async-await promise generator bluebird
2021-01-15 11:42:43

我正在使用生成器和 Bluebird 编写代码,我有以下内容:

var async = Promise.coroutine;
function Client(request){
    this.request = request;
}


Client.prototype.fetchCommentData = async(function* (user){
    var country = yield countryService.countryFor(user.ip);
    var data = yield api.getCommentDataFor(user.id);
    var notBanned = yield authServer.authenticate(user.id);
    if (!notBanned) throw new AuthenticationError(user.id);
    return {
        country: country,
        comments: data,
        notBanned: true
    };
});

但是,这有点慢,我觉得我的应用程序等待 I/O 的时间太长,而且不是并行的。如何提高应用程序的性能?

总响应时间为 800 为countryFor+ 400 为getCommentDataFor+ 600 为authenticate所以总共 1800 毫秒,这是很多。

3个回答

您花费了太多时间等待来自不同来源的 I/O。

然而,在普通的 Promise 代码中,您会使用Promise.all它 - 人们倾向于编写代码来等待生成器的请求。您的代码执行以下操作:

<-client     service->
countryFor..
           ''--..
              ''--..
                 ''--.. country server sends response
               ..--''
          ..--''
     ..--''
getCommentDataFor
     ''--..
           ''--..
               ''--..
                     ''--.. comment service returns response
                ..--''
          ..--''
      ..--''
authenticate
       ''--..
            ''--..
                  ''--.. authentication service returns
             ..--''
       ..--''
 ..--''
 Generator done.

相反,它应该这样做:

<-client     service->
countryFor..
commentsFor..''--..
authenticate..''--..''--..
                 ''--..''--..''--.. country server sends response
                        ''--..--''..  comment service returns response
                   ..--''..--''..     authentication service returns response
          ..--''..--''..
 ..--''..--''..--''
 ..--''..--''
 ..--''
 Generator done

简而言之,您的所有 I/O 都应该在这里并行完成。

为了解决这个问题,我会使用Promise.props. Promise.props接受一个对象并等待其所有属性解析(如果它们是Promise)。

请记住 - 生成器和Promise混合搭配得非常好,您只需产生Promise:

Client.prototype.fetchCommentData = async(function* (user){
    var country = countryService.countryFor(user.ip);
    var data = api.getCommentDataFor(user.id);
    var notBanned = authServer.authenticate(user.id).then(function(val){
          if(!val) throw new AuthenticationError(user.id);
    });
    return Promise.props({ // wait for all promises to resolve
        country : country,
        comments : data,
        notBanned: notBanned
    });
});

这是人们第一次使用生成器时常犯的错误。

克里斯·科瓦尔 (Kris Kowal) 无耻地从 Q-Connection 中获取了 ascii 艺术

countryService.countryFor(user.ip)同步组网吗?我没有看到任何允许它异步的回调或Promise。
2021-03-23 11:42:43
+1 不仅是为了这个答案的清晰性,而且是为了第三人称参考。
2021-03-30 11:42:43
@Bergi 一定要加油 :)
2021-03-30 11:42:43
@Bergi 在生成器中,执行 areturn实际上就像 a yieldonlydone标志也设置为 true,所以从技术上讲,它使用的是生成器。我同意生成器不再有用了。我对此进行问答是因为我看到人们一直在使用生成器来解决这个用例错误,一个更复杂的生成器示例觉得这些东西不值得教学value,我仍然愿意接受改进建议。
2021-04-05 11:42:43
@BenjaminGruenbaum:我会回答的 :-)
2021-04-05 11:42:43

正如 Bluebird 文档中提到的Promise.coroutine,您需要注意不要yield在一个系列中。

var county = yield countryService.countryFor(user.ip);
var data = yield api.getCommentDataFor(user.id);
var notBanned = yield authServer.authenticate(user.id);

这段代码有 3 个yield表达式,每个表达式都停止执行,直到特定的 Promise 被解决。该代码将连续创建和执行每个异步任务。

要并行等待多个任务,您应该yield使用promises 数组这将等到所有这些都得到解决,然后返回一个结果值数组。使用 ES6 解构赋值可以得到简洁的代码:

Client.prototype.fetchCommentData = async(function* (user){
    var [county, data, notBanned] = yield [
//             a single yield only: ^^^^^
        countryService.countryFor(user.ip),
        api.getCommentDataFor(user.id),
        authServer.authenticate(user.id)
    ];
    if (!notBanned)
        throw new AuthenticationError(user.id);
    return {
        country: country,
        comments: data,
        notBanned: true
    };
});
Bluebird 是否支持生成一系列开箱即用的 promise?
2021-03-23 11:42:43
+1 虽然节点 11 没有像这样的解构
2021-03-29 11:42:43
@rane:在当前版本中看起来不像。请参阅最后一个示例,addYieldHandler尽管它添加了此功能,如果您不喜欢,可以使用Promise.all
2021-04-05 11:42:43

Benjamin Gruenbaum 的答案是正确的,但它完全失去了生成器方面,当您尝试并行运行多个事物时,这往往会发生一点。但是,您可以使用yield关键字使这项工作正常进行我还使用了一些额外的 ES6 特性,比如解构赋值对象初始值设定项简写

Client.prototype.fetchCommentData = async(function* (user){
    var country = countryService.countryFor(user.ip);
    var data = api.getCommentDataFor(user.id);
    var notBanned = authServer.authenticate(user.id).then(function(val){
        if(!val) throw new AuthenticationError(user.id);
    });

    // after each async operation finishes, reassign the actual values to the variables
    [country, data, notBanned] = yield Promise.all([country, data, notBanned]);

    return { country, data, notBanned };
});

如果你不想使用那些额外的 ES6 特性:

Client.prototype.fetchCommentData = async(function* (user){
    var country = countryService.countryFor(user.ip);
    var data = api.getCommentDataFor(user.id);
    var notBanned = authServer.authenticate(user.id).then(function(val){
        if(!val) throw new AuthenticationError(user.id);
    });

    var values = yield Promise.all([country, data, notBanned]);

    return { 
        country: values[0], 
        data: values[1], 
        notBanned: values[2]
    };
});
我认为下面的例子更好地表达Promise.props为如上所示。
2021-04-13 11:42:43
你说得对。我只是坚持 ES6 API 的Promise,因为这是我真正知道的。
2021-04-13 11:42:43