Переведено с:Faster async functions and promises
Асинхронный процесс JavaScript всегда считался недостаточно быстрым, и, что еще хуже, отладка в требовательных сценариях в реальном времени, таких как NodeJS, превращается в кошмар. Однако это меняется, и в этом посте подробно объясняется, как мы оптимизировали асинхронные функции и промисы в движке V8 (а также в некоторых других движках), а также о сопутствующей оптимизации процесса разработки.
Советы:вотвидео, вы можете прочитать его вместе со статьей.
Новый подход к асинхронному программированию
От обратных вызовов к обещаниям и асинхронным функциям
До того, как промисы официально стали частью стандарта JavaScript, обратные вызовы широко использовались в асинхронном программировании.Вот пример:
function handler(done) {
validateParams((error) => {
if (error) return done(error);
dbQuery((error, dbResults) => {
if (error) return done(error);
serviceCall(dbResults, (error, serviceResults) => {
console.log(result);
done(error, serviceResults);
});
});
});
}
Глубоко вложенные обратные вызовы, подобные приведенным выше, часто называют «черными дырами обратных вызовов», потому что они делают код менее читабельным и менее удобным для сопровождения.
К счастью, теперь, когда обещания являются частью языка JavaScript, следующий код реализует ту же функциональность, что и выше:
function handler() {
return validateParams()
.then(dbQuery)
.then(serviceCall)
.then(result => {
console.log(result);
return result;
});
}
В последнее время JavaScript поддерживаетасинхронная функция, приведенный выше асинхронный код можно записать как синхронный код, как показано ниже:
async function handler() {
await validateParams();
const dbResults = await dbQuery();
const results = await serviceCall(dbResults);
console.log(results);
return results;
}
С помощью асинхронных функций код становится более лаконичным, а логика и поток данных кода становятся более управляемыми.Конечно, базовая реализация по-прежнему асинхронна. (Обратите внимание, что JavaScript по-прежнему является однопоточным, а асинхронные функции не открывают новые потоки.)
Обратный вызов от прослушивателя событий к асинхронному итератору
В NodeJSReadableStreamsАсинхронный как другая форма также особенно распространен, вот пример:
const http = require('http');
http.createServer((req, res) => {
let body = '';
req.setEncoding('utf8');
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
res.write(body);
res.end();
});
}).listen(1337);
Этот код немного сложен для понимания: поток данных в чанках можно получить только через обратный вызов, а конец потока данных также должен быть обработан в обратном вызове. Если вы не понимаете, что функция завершается немедленно, но фактическая обработка должна происходить в обратном вызове, вы можете ввести ошибки.
Также, к счастью, появилась классная функция ES2018.асинхронный итераторПриведенный выше код можно упростить:
const http = require('http');
http.createServer(async (req, res) => {
try {
let body = '';
req.setEncoding('utf8');
for await (const chunk of req) {
body += chunk;
}
res.write(body);
res.end();
} catch {
res.statusCode = 500;
res.end();
}
}).listen(1337);
Вы можете поместить всю логику обработки данных в асинхронную функцию.for await…of
перебирать куски, а не по отдельности'data'
а также'end'
Он обрабатывается в обратном вызове, и мы также добавилиtry-catch
блокировать, чтобы избежатьunhandledRejection
вопрос.
Вы можете использовать эти функции в своей среде сборки уже сегодня! асинхронная функцияПолностью поддерживается, начиная с Node.js 8 (V8 v6.2/Chrome 62)., асинхронный итераторПоддерживается с Node.js 10 (V8 v6.8/Chrome 68).
асинхронная оптимизация производительности
От V8 v5.5 (Chrome 55 и Node.js 7) до V8 v6.8 (Chrome 68 и Node.js 10) мы работали над оптимизацией производительности для асинхронного кода, пока результаты довольно хорошие, вы можете использовать их с уверенностью Новые функции.
выше этотест доксби, используемый для отображения производительности при интенсивном использовании промисов Вертикальная ось на рисунке представляет время выполнения, поэтому чем меньше, тем лучше.
с другой стороны,параллельный тестРеагирует на интенсивное использованиеPromise.all()производительность, результаты следующие:
Promise.all
производительность улучшенавосемь раз!
Тогда приведенный выше тест - это всего лишь небольшой тест уровня DEMO, команда V8 больше беспокоится оЭффект оптимизации фактического пользовательского кода.
Вышеприведенные тесты основаны на популярных на рынке HTTP-фреймворках, в которых активно используются промисы иasync
Функция, эта таблица показывает количество запросов в секунду, поэтому он отличается от предыдущей таблицы, тем больше значение. Из таблицы можно увидеть, что есть много производительности от Node.js 7 (v8 v5.5) до node.js 10 (v8 v6.8).
Прирост производительности зависит от трех факторов:
- TurboFan, новый оптимизирующий компилятор 🎉
- Orinoco, новый сборщик мусора 🚛
- Ошибка Node.js 8 заставляла await пропускать некоторые микротики 🐛
когда мыNode.js 8внутриВключить турбовентилятор, производительность была значительно улучшена.
В то же время мы представили новый сборщик мусора под названием Orinoco, который убирает сборку мусора из основного потока, поэтому очень помогает улучшить скорость ответа на запросы.
Наконец, в Node.js 8 появилась ошибка, из-за которой в какой-то моментawait
Пропустите несколько микротиков, которые вместо этого улучшат производительность. Эта ошибка была вызвана непреднамеренным нарушением спецификации, но она дала нам некоторые идеи для оптимизации. Здесь мы немного объясним:
const p = Promise.resolve();
(async () => {
await p; console.log('after:await');
})();
p.then(() => console.log('tick:a'))
.then(() => console.log('tick:b'));
Приведенный выше код изначально создает обещание с завершенным состоянием.p
,Потомawait
В результате две цепи были скованы одновременно.then
, финалconsole.log
Что получится в результате печати?
потому чтоp
сделано, вы можете подумать, что сначала будет напечатано'after:await'
, то оставшиеся дваtick
, на самом деле результат в Node.js 8:
Хотя приведенные выше результаты соответствуют ожиданиям, они не соответствуют спецификации. Node.js 10 исправляет это поведение и будет выполняться первымthen
в цепочке, а затем асинхронная функция.
Это «правильное поведение» не кажется нормальным и даже удивляет многих разработчиков JavaScript, и его стоит подробно объяснить. Прежде чем объяснять, давайте начнем с некоторых основ.
Задачи против микрозадач
На одном уровне в JavaScript есть задачи и микрозадачи. Задачи обрабатывают события, такие как ввод-вывод и таймеры, по одному. Микрозадачи предназначены дляasync
/await
Он разработан с отложенным выполнением промисов, и каждая задача выполняется последней. Очередь микрозадач очищается перед возвратом в цикл обработки событий.
Доступно через Джейка Арчибальдаtasks, microtasks, queues, and schedules in the browserпонять больше. Модель задач в Node.js очень похожа.
асинхронная функция
Согласно MDN, асинхронная функция — это функция, которая выполняется асинхронно и в результате неявно возвращает промис. С точки зрения разработчика, асинхронные функции делают асинхронный код похожим на синхронный.
Простейшая асинхронная функция:
async function computeAnswer() {
return 42;
}
Когда функция выполняется, она возвращает обещание, и вы можете использовать возвращенное значение так же, как и любое другое обещание.
const p = computeAnswer();
// → Promise
p.then(console.log);
// prints 42 on the next turn
Вы можете получить промис только после выполнения следующей микрозадачиp
Возвращаемое значение, другими словами, приведенный выше код семантически эквивалентен использованиюPromise.resolve
Полученные результаты:
function computeAnswer() {
return Promise.resolve(42);
}
Настоящая сила асинхронных функций исходит изawait
Выражение, которое приостанавливает выполнение функции до тех пор, пока обещание не будет разрешено, а затем возобновляет выполнение, когда оно выполнено. Выполненное обещание будет действовать какawait
ценность . Вот пример, объясняющий такое поведение:
async function fetchStatus(url) {
const response = await fetch(url);
return response.status;
}
fetchStatus
при встречеawait
останавливается, когдаfetch
Это обещание возобновит выполнение после его завершения, которое напрямую связаноfetch
Возвращаемое обещание несколько эквивалентно.
function fetchStatus(url) {
return fetch(url).then(response => response.status);
}
Функция обработки цепочки содержит следующиеawait
код позади.
В норме вы должны бытьawait
поставить один позадиPromise
, но на самом деле за ним может следовать любое значение JavaScript.Если за ним не следует промис, он будет преобразован в промис, поэтомуawait 42
Эффект следующий:
async function foo() {
const v = await 42;
return v;
}
const p = foo();
// → Promise
p.then(console.log);
// prints `42` eventually
Более интересно,await
может сопровождаться любым"тогда", например, любой, содержащийthen
Объект метода, даже если это не обещание. Таким образом, вы можете реализовать интересный класс для регистрации потребления времени выполнения:
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(() => resolve(Date.now() - startTime),
this.timeout);
}
}
(async () => {
const actualTime = await new Sleep(1000);
console.log(actualTime);
})();
Давайте посмотрим на V8Технические характеристикикак бороться сawait
из. Ниже приведена очень простая асинхронная функция.foo
:
async function foo(v) {
const w = await v;
return w;
}
При выполнении принимает параметрv
Оберните это как обещание, затем сделайте паузу, пока обещание не будет выполнено, затемw
Назначено выполненному обещанию, и, наконец, async вернул это значение.
таинственныйawait
Во-первых, V8 пометит функцию как возобновляемую, что означает, что выполнение можно приостановить и возобновить (сawait
Вот так это выглядит). Затем так называемаяimplicit_promise
(Используется для преобразования значений, созданных в асинхронных функциях, в промисы).
Затем появилось кое-что интересное: настоящаяawait
. Во-первых, следуйтеawait
Последующие значения преобразуются в обещания. Затем функция-обработчик свяжет это обещание, чтобы возобновить основную функцию после завершения обещания.В это время асинхронная функция приостанавливается и возвращаетсяimplicit_promise
вызывающему абоненту. однаждыpromise
сделано, функция возобновит работу и получитpromise
получить значениеw
,наконец,implicit_promise
буду использоватьw
Отметить как принятое.
просто скажи,await v
Этапы инициализации состоят из следующего:
- Пучок
v
Превратиться в обещаниеawait
Назад). - Обработчики привязки используются для пост-восстановления.
- Приостановить асинхронную функцию и вернуться
implicit_promise
вызывающему абоненту.
Давайте рассмотрим это шаг за шагом, предполагая, чтоawait
за которым следует обещание, а значение конечного завершенного состояния равно42
. Затем движок создает новыйpromise
и положиawait
Значение после него используется как значение разрешения. с помощью стандартаPromiseResolveThenableJobЭти обещания будут выполнены в следующем цикле.
Затем движок создает другой, называемыйthrowaway
обещать. Он назван так, потому что больше ничего с ним не связывает, он предназначен только для внутреннего использования в движке.throwaway
Обещание будет привязано к обещанию с обработчиком восстановления.promise
начальство. здесьperformPromiseThen
Операция на самом деле внутреннеPromise.prototype.then(). В конце концов, асинхронная функция приостанавливается и передает управление вызывающей стороне.
Вызывающая программа будет продолжать выполняться, в конечном итоге стек вызовов будет опустошен, после чего движок начнет выполнять микрозадачи:PromiseResolveThenableJob, первыйPromiseReactionJob, его задача только пройтиawait
инкапсулировать слой по значениюpromise
. Затем механизм возвращается к очереди микрозадач, потому что очередь микрозадач должна быть очищена перед возвратом в цикл обработки событий.
затем еще одинPromiseReactionJob, ждем мыawait
(имеется в виду здесь42
)этоpromise
сделано, затем запланируйте это действие наthrowaway
в обещании. Механизм продолжает возвращаться к очереди микрозадач, потому что осталась последняя микрозадача.
Теперь эта секундаPromiseReactionJobсообщить о решенииthrowaway
обещание и возобновить выполнение асинхронной функции, наконец, вернувшись изawait
принадлежит42
.
Подводя итог, для каждогоawait
двигатель создастдва дополнительныхобещание (даже если rvalue уже является обещанием) и требуетне менее трехмикрозадачи. Кто бы мог подумать о простомawait
Как могло быть так много избыточных операций? !
Давайте посмотрим, что именно вызывает избыточность. Функция первой строки — инкапсулировать промис, а второй строки — разрешить инкапсулированный промис.await
значение послеv
. Эти две линии производят одно избыточное обещание и две избыточные микрозаски. еслиv
Это уже обещание, оно того не стоит (большинство верно). В некоторых специальных сценахawait
охватывать42
Если это так, его действительно нужно инкапсулировать в обещание.
Итак, здесь вы можете использоватьpromiseResolveОперации обрабатываются, а промисы инкапсулируются только при необходимости:
Если входной параметр является промисом, он будет возвращен без изменений, и будут инкапсулированы только необходимые промисы. Эта операция сохраняет дополнительное обещание и две микрозадачи, когда значение уже является обещанием. Эта функция может быть--harmony-await-optimization
Параметр включен в V8 (начиная с V7.1), и мыЯ сделал предложение Ecmascript, который, как ожидается, скоро будет объединен.
Ниже приведен упрощенныйawait
Процесс реализации:
спасибо удивительноpromiseResolve, теперь нам просто нужно пройтиv
Просто не важно, что это такое. Затем, как и раньше, движок создаетthrowaway
обещай и ставьPromiseReactionJobЧтобы возобновить асинхронную функцию на следующем тике, она приостанавливает функцию и возвращается вызывающей стороне.
Когда все выполнения будут завершены в конце, движок запустит очередь микрозадач и выполнитPromiseReactionJob. Эта задача пройдетpromise
результат даетthrowaway
и восстановить асинхронную функцию изawait
получать42
.
Несмотря на внутреннее использование, двигатель создаетthrowaway
Обещания могут все еще казаться неправильными. Оказывается, чтоthrowaway
обещания только для удовлетворения спецификацииperformPromiseThen
потребности.
Недавно это было предложено ECMAScript.изменять, двигатель больше не должен создавать большую часть времениthrowaway
.
В сравненииawait
Производительность на Node.js 10 и оптимизированная (следует поставить на Node.js 12):
async
/await
Превосходит написанный от руки код обещания. Суть в том, что благодаря этому мы уменьшили некоторые ненужные накладные расходы в асинхронной функции не только в движке V8, но и в других движках JavaScript.пластырьОптимизировано.
Оптимизация опыта разработки
Помимо производительности, разработчики JavaScript также очень озабочены поиском и устранением проблем, что никогда не было легко в асинхронном коде.Chrome DevToolsТеперь поддерживаются асинхронные трассировки стека:
Это полезная функция при локальной разработке, но она бесполезна после развертывания приложения. При отладке видно только в лог-файлеError#stack
информации, они не содержат никакой асинхронной информации.
Что мы делали недавноАсинхронная трассировка стека с нулевой стоимостьюсделатьError#stack
Содержит информацию о вызовах для асинхронных функций. «Нулевая стоимость» звучит захватывающе, не так ли? Как функция Chrome DevTools может обеспечить нулевую стоимость, если она сопряжена со значительными накладными расходами? Например,foo
вызыватьbar
,bar
Выбросить исключение после ожидания обещания:
async function foo() {
await bar();
return 42;
}
async function bar() {
await Promise.resolve();
throw new Error('BEEP BEEP');
}
foo().catch(error => console.log(error.stack));
Запуск этого кода на Node.js 8 или Node.js 10 будет выглядеть так:
$ node index.js
Error: BEEP BEEP
at bar (index.js:8:9)
at process._tickCallback (internal/process/next_tick.js:68:7)
at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
at startup (internal/bootstrap/node.js:266:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3)
отметить, что, несмотря наfoo()
Звонок выдал ошибку,foo
Его нет в самой трассировке стека. Если приложение развернуто в облачном контейнере, разработчикам может быть сложно найти проблему.
Интересно, что двигатель знает об этом.bar
Что должно продолжать выполняться после окончания: т.е.foo
в функцииawait
назад. Вот именно, вотfoo
Место для паузы. Механизм может использовать эту информацию для восстановления асинхронных трассировок стека. С приведенными выше оптимизациями вывод будет выглядеть следующим образом:
$ node --async-stack-traces index.js
Error: BEEP BEEP
at bar (index.js:8:9)
at process._tickCallback (internal/process/next_tick.js:68:7)
at Function.Module.runMain (internal/modules/cjs/loader.js:745:11)
at startup (internal/bootstrap/node.js:266:19)
at bootstrapNodeJSCore (internal/bootstrap/node.js:595:3)
at async foo (index.js:2:3)
В трассировке стека сначала появляется функция верхнего уровня, за которой следуют несколько стеков асинхронных вызовов, а затемfoo
вbar
Информация о стеке контекста. Эта функция может быть включена через V8.--async-stack-traces
параметр включен.
Однако, если вы сравните информацию о стеке в Chrome DevTools выше, вы увидите, что асинхронная часть трассировки стека отсутствует.foo
информация о точке вызова. используется здесьawait
Позиция возобновления и паузы — это одна и та же функция, ноPromise#then()илиPromise#catch()Не так. См. статью Матиаса Байненса.await beats Promise#then()понять больше.
В заключение
Следующие две оптимизации необходимы для того, чтобы асинхронные функции работали быстрее:
- Убраны две лишние микрозадачи
- удаленный
throwaway
promise
Кроме того, мы проходимАсинхронная трассировка стека с нулевой стоимостьюпродвигаетсяawait
а такжеPromise.all()
Опыт разработки и отладки.
У нас также есть несколько советов по повышению производительности, удобных для разработчиков JavaScript:
использовать большеasync
а такжеawait
Вместо того, чтобы писать код обещания вручную, используйте обещание, предоставленное движком JavaScript, вместо того, чтобы реализовывать его самостоятельно.
Статья может быть воспроизведена по желанию, но просьба сохранитьОригинальная ссылка. Добро пожаловать!ES2049 Studio, отправьте свое резюме на caijun.hcj(at)alibaba-inc.com.