Замедление из-за непараллельного ожидания promises в асинхронных генераторах

Я пишу код с использованием генераторов и 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
    };
});

Однако, это довольно медленно, я чувствую, что мое приложение слишком много ждет для ввода-вывода, и это не параллельно. Как повысить производительность моего приложения?

Общее время отклика составляет 800 для countryFor + 400 для getCommentDataFor + 600 для authenticate, поэтому всего 1800 мс, что много.

Ответ 1

Вы тратите слишком много времени на ожидание ввода-вывода из разных источников.

В нормальном коде обещания вы использовали бы 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

Проще говоря, все ваши операции ввода-вывода должны выполняться параллельно.

Чтобы исправить это, я бы использовал Promise.props. Promise.props принимает объекты и ждет, пока все его свойства будут разрешены (если они promises).

Помните - генераторы и promises хорошо сочетаются и соответствуют, вы просто получаете promises:

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

Это очень распространенная ошибка, которую люди делают при использовании генераторов в первый раз.

искусство ascii бесстыдно взято из Q-Connection Крисом Ковалем

Ответ 2

Как уже упоминалось в документах 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, каждый из которых останавливает выполнение до тех пор, пока не будет определено конкретное обещание. Код будет создавать и выполнять каждую из задач async последовательно.

Чтобы подождать несколько задач параллельно, вы должны 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
    };
});

Ответ 3

Ответ 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]
    };
});