21

I'm writing code using generators and Bluebird and I have the following:

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
    };
});

However, this is kind of slow, I feel like my application is waiting too much for I/O and it's not in parallel. How can I improve the performance of my application?

The total response time is 800 for countryFor + 400 for getCommentDataFor + 600 for authenticate so in total 1800ms which is a lot.

4

3 回答 3

20

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

然而,在普通的承诺代码中,你会使用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接受一个对象并等待其所有属性解析(如果它们是承诺)。

请记住 - 生成器和承诺非常好地混合和匹配,您只需生成承诺:

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 艺术

于 2014-06-12T20:44:20.513 回答
12

正如 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表达式,每个表达式都会停止执行,直到特定的承诺得到解决。代码将连续创建和执行每个异步任务。

要并行等待多个任务,您应该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
    };
});
于 2014-06-12T20:56:53.843 回答
4

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]
    };
});
于 2014-11-17T03:45:38.093 回答