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

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

3 ответа

Решение

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

В обычном коде обещания вы бы использовали 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 берет объект и ждет разрешения всех его свойств (если они являются обещаниями).

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

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 Крисом Ковалем

Как это упоминается в документации 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 массив обещаний. Это будет ждать, пока все они не будут установлены, а затем вернет массив значений результата. Использование 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
    };
});

Ответ Бенджамина Грюнбаума верен, но он полностью теряет аспект генератора, что обычно бывает немного, когда вы пытаетесь запустить несколько вещей параллельно. Вы можете, однако, сделать эту работу очень хорошо с 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]
    };
});
Другие вопросы по тегам