Автор материала, перевод которого мы сегодня публикуем, рассказывает о том, что такое ад async/await, и о том, как из него сбежать.
Что такое ад async/await
В процессе работы с асинхронными возможностями JavaScript программисты часто используют конструкции, состоящие из множества вызовов функций, перед каждым из которых ставится ключевое слово
await
. Так как часто в подобных случаях выражения с await
независимы друг от друга, подобный код приводит к проблемам с производительностью. Дело тут в том, что прежде чем система сможет заняться следующей функцией, ей необходимо дождаться завершения выполнения предыдущей функции. Это и есть ад async/await.Пример: заказ пиццы и напитков
Представим, что нам надо написать скрипт, предназначенный для оформления заказов на пиццу и напитки. Этот скрипт может выглядеть так:
(async () => {
const pizzaData = await getPizzaData() // асинхронный вызов
const drinkData = await getDrinkData() // асинхронный вызов
const chosenPizza = choosePizza() // синхронный вызов
const chosenDrink = chooseDrink() // синхронный вызов
await addPizzaToCart(chosenPizza) // асинхронный вызов
await addDrinkToCart(chosenDrink) // асинхронный вызов
orderItems() // асинхронный вызов
})()
На первый взгляд скрипт выглядит вполне нормально, к тому же — он работает так, как ожидается. Однако при внимательном рассмотрении этого кода оказывается, что его реализация хромает, так как тут не учитываются особенности асинхронного выполнения кода. Разобравшись с тем, что именно здесь не так, мы сможем решить проблему этого скрипта.
Код обёрнут в асинхронное немедленно вызываемое функциональное выражение (IIFE). Обратите внимание на то, что все задачи выполняются именно в том порядке, в котором они приведены в коде, при этом, для того, чтобы перейти к следующей задаче, нужно дождаться выполнения предыдущей. А именно, вот что здесь происходит:
- Получение списка видов пиццы.
- Получение списка напитков.
- Выбор пиццы из списка.
- Выбор напитка из списка.
- Добавление выбранной пиццы в корзину.
- Добавление выбранного напитка в корзину.
- Оформление заказа.
Выше сделан акцент на том, что операции в скрипте выполняются строго последовательно. Здесь не используются возможности параллельного выполнения кода. Поразмыслим над следующим: почему мы ожидаем получения списка видов пиццы для того, чтобы начать загрузку списка напитков? Следовало бы выполнять эти задачи одновременно. Однако, для того, чтобы получить возможность выбрать пиццу из списка, сначала надо дождаться загрузки списка видов пиццы. То же самое относится и к процессу выбора напитка.
В результате можно прийти к выводу, что задачи, связанные с пиццей, и задачи, связанные с напитками, могут выполняться параллельно, но отдельные операции, относящиеся только к пицце (или только к напиткам) должны выполняться последовательно.
Пример: оформление заказа на основе содержимого корзины
Вот пример кода, в котором осуществляется загрузка данных о содержимом корзины и отправка запроса на формирование заказа:
async function orderItems() {
const items = await getCartItems() // асинхронный вызов
const noOfItems = items.length
for(var i = 0; i < noOfItems; i++) {
await sendRequest(items[i]) // асинхронный вызов
}
}
В данном случае циклу
for
приходится ждать завершения каждого вызова функции sendRequest()
для того, чтобы перейти к следующей итерации. Однако, мы, на самом деле, не нуждаемся в этом ожидании. Мы хотим выполнить все запросы как можно быстрее, а затем дождаться их завершения.Надеюсь, теперь вы приблизились к пониманию сущности ада async/await, и того, насколько сильно он может повлиять на производительность приложений. Теперь подумайте над вопросом, вынесенным в заголовок следующего раздела.
Что если забыть воспользоваться ключевым словом await?
Если забыть воспользоваться ключевым словом
await
при вызове асинхронной функции, то функция просто начнёт выполняться. Такая функция вернёт промис, который можно использовать позже.(async () => {
const value = doSomeAsyncTask()
console.log(value) // неразрешённый промис
})()
Ещё одно следствие вызова асинхронной функции без
await
заключается в том, что компилятор не будет знать о том, что программист хочет дождаться полного завершения выполнения функции. В результате компилятор выйдет из программы, не завершив асинхронную задачу. Поэтому не следует забывать о ключевом слове await
там, где оно необходимо.У промисов есть интересное свойство: в одной строке кода промис можно получить, а в другой — дождаться его разрешения. Этот факт и является ключом к побегу из ада async/await.
(async () => {
const promise = doSomeAsyncTask()
const value = await promise
console.log(value) // реальное значение
})()
Как видите, вызов
doSomeAsyncTask()
возвращает промис. В этот момент данная функция начинает выполняться. Для того чтобы получить результат разрешения промиса, мы используем ключевое слово await
, сообщая тем самым системе, что ей не следует немедленно выполнять следующую строку кода. Вместо этого надо дождаться разрешения промиса, а уже потом переходить к следующей строке.Как выбраться из ада async/await?
Для того чтобы выбраться из ада async/await, можно воспользоваться следующим планом действий.
?1. Найдите выражения, которые зависят от выполнения других выражений
В первом примере был показан скрипт для выбора пиццы и напитка. Мы решили, что, прежде чем у нас появится возможность выбрать пиццу из списка, нам надо загрузить список видов пиццы. А прежде чем добавить пиццу в корзину, нужно её выбрать. В результате можно сказать, что эти три шага зависят друг от друга. Нельзя перейти к следующему шагу, не завершив предыдущий.
Теперь, если мыслить шире и подумать о напитках, окажется, что процесс выбора пиццы не зависит от процесса выбора напитка, поэтому эти две задачи можно распараллелить. Компьютеры очень хорошо справляются с выполнением параллельных задач.
В итоге мы поняли, какие именно выражения зависят друг от друга, а какие — нет.
?2. Сгруппируйте зависимые выражения в отдельных асинхронных функциях
Как мы уже выяснили, процесс выбора пиццы состоит из нескольких шагов: загрузка списка видов пиццы, выбор конкретной пиццы и добавление её в корзину. Именно эти действия и надо собрать в отдельную асинхронную функцию. Не забывая о том, что похожая последовательность действий характерна и для напитков, мы приходим к двум асинхронным функциям, которые можно назвать
selectPizza()
и selectDrink()
.?3. Выполните полученные асинхронные функции параллельно
Теперь воспользуемся возможностями цикла событий JavaScript для того, чтобы организовать параллельное неблокирующее выполнение полученных асинхронных функций. Тут применяются два распространённых паттерна — ранний возврат промисов и метод
Promise.all()
.Работа над ошибками
Применим на практике три вышеописанных шага по избавлению от ада async/await. Исправим вышеприведённые примеры. Вот как теперь будет выглядеть первый.
async function selectPizza() {
const pizzaData = await getPizzaData() // асинхронный вызов
const chosenPizza = choosePizza() // синхронный вызов
await addPizzaToCart(chosenPizza) // асинхронный вызов
}
async function selectDrink() {
const drinkData = await getDrinkData() // асинхронный вызов
const chosenDrink = chooseDrink() // синхронный вызов
await addDrinkToCart(chosenDrink) // асинхронный вызов
}
(async () => {
const pizzaPromise = selectPizza()
const drinkPromise = selectDrink()
await pizzaPromise
await drinkPromise
orderItems() // асинхронный вызов
})()
// Задачу можно решить так, как показано выше, но я предпочитаю следующий метод
(async () => {
Promise.all([selectPizza(), selectDrink()]).then(orderItems) // асинхронный вызов
})()
Теперь выражения, относящиеся к пицце и напиткам, сгруппированы в функциях
selectPizza()
и selectDrink()
. Внутри этих функций важен порядок выполнения команд, так как следующие команды зависят от результатов выполнения предыдущих. После того, как функции подготовлены, мы вызываем их асинхронно.Во втором примере нам приходится иметь дело с неизвестным количеством промисов. Однако решить эту проблему очень просто. А именно, надо создать массив и поместить в него промисы. Затем, используя
Promise.all()
, можно организовать ожидание разрешения всех этих промисов.async function orderItems() {
const items = await getCartItems() // асинхронный вызов
const noOfItems = items.length
const promises = []
for(var i = 0; i < noOfItems; i++) {
const orderPromise = sendRequest(items[i]) // асинхронный вызов
promises.push(orderPromise) // синхронный вызов
}
await Promise.all(promises) // асинхронный вызов
}
Итоги
Как видите, то, что называется «адом async/await», на первый взгляд выглядит вполне прилично, однако, за внешним благополучием кроется негативное воздействие на производительность. Из этого ада, однако, не так уж и сложно сбежать. Достаточно проанализировать код, выяснить, какие задачи, решаемые с его помощью, можно распараллелить, и внести в программу необходимые изменения.
Уважаемые читатели! Доводилось ли вам видеть ад async/await?
Комментарии (47)
rumkin
17.04.2018 15:44Мой друзья! Лучше использовать инструменты наподобие
map
библиотеки Bluebird, которые позволяют управлять количеством одновременно запущенных промисов:
Bluebird.map( array, item => doSomethingAsync(item), {concurrency: LIMIT} )
Это защитит память приложения от переполнения при обработке огромных массивов и предотвратит возникновение блокировок.
Перевод моего комментария к этой статье.
morsic
18.04.2018 12:04>Это защитит память приложения от переполнения при обработке огромных массивов
Может сразу Stream api использовать?
Промисы по своей сути не особо подходят для больших обьемов данных.rumkin
20.04.2018 00:12Так-то они тоже теперь на промисы переезжают. Поэтому вполне достаточно использовать
Bluebird.map
илиp-limit
, как посоветовали выше.
indestructable
17.04.2018 15:46+2// Задачу можно решить так, как показано выше, но я предпочитаю следующий метод (async () => { Promise.all([selectPizza(), selectDrink()]).then(orderItems) // асинхронный вызов })()
Разве здесь не нужен
await
(или убрать фигурные скобки вокруг тела функции)?
Статья как по мне спорная. Не увидел особого "ада", в основном, начальные примеры показывают, что бывает, если не понимаешь, как работает
async/await
.
Как по мне, разобраться с этим уж точно не сложнее, чем с промисами. А код получается на порядки читабельнее.
apapacy
17.04.2018 15:53+1У async/await совсем другие выявились недостатки. А именно необходимость заключать их в блоки try/catch которые с промисами выглядят порой немного лаконичнее как вызов функций then()/catch(). Без try/catch код выглядит действительно очень неплохо. C try/catch все опять становится весьма многоэтажно.
Все это погружать в Promise.all() конечно рационально но сразу теряется наглядность. Я где-то видел более естественное решение
const promiseSomeWhat = someWhat(); const promiseAnother = another(); const someData = await promiseSomeWhat; const anotherData = await promiseAnother;
arvitaly
19.04.2018 05:10Я вот написал библиотеку with-error, оборачивающую функции с исключениями и многоэтажность исчезла. А вообще лучше не пользоваться в JS исключениями, крайне неудобная работа с ними, особенно в области типизации.
iShatokhin
17.04.2018 16:43+2Последний блок можно написать компактней:
const promises = items.map(async (item) => { await sendRequest(item); }); await Promise.all(promises);
indestructable
17.04.2018 20:42Потеряли return или лишние фигурные скобки.
Или бабель добавляет туда return сам? Просто в самой статье тоже была такая же ошибка.
iShatokhin
17.04.2018 20:48А нужен ли там return?
Последняя строчка не ожидает результатов по завершению Promise.allapapacy
17.04.2018 22:58Автору оригинальной статьи уже сделали замечание по этому поводу и он принес свои извинения. Правда статью не откорректировал пока.
indestructable
18.04.2018 09:31return нужен внутри map, иначе Promise.all ничего не дождётся.
Alternator
18.04.2018 09:39Отсутствие return означает возврат Promise в данном случае.
Таким образом в promises попадет массив промисов, которые зарезолвятся в undefined после окончания соответствующих sendRequest.
Promise.all дождется факта окончания всех операций, и вернет массив undefined-ов, вместо результатов.
keenondrums
17.04.2018 17:50+2Зачем при рефакторинге этот вызов делать асинхронной функцией?
(async () => {
Promise.all([selectPizza(), selectDrink()]).then(orderItems) // асинхронный вызов
})()
Если уж сделали асинхронной, то почему бы тогда не дождаться резолва Promise.all с помощью await? Promise.all внезано тоже возвращает промис.
Сами себе каких-то проблем выдумали, сами их себе порешали.
AxisPod
17.04.2018 21:26Очередной перевод статьи, автор которой не знает async/await, которому лень открыть тот же MDN и хотя бы посмотреть примеры готовки. Уже оставлял в недавнем вашем переводе коммент по этому поводу. async/await это не промисы, не вводите в заблуждение людей переводами таких статей.
Тут не нужено использовать Promise.all в принципе. Все await в рамках одного выражения отрабатывают параллельно, но никто чего-то этим даже не пытается пользоваться. В итоге мы имеем статьи вида: Чукча не читатель, чукча писатель.
Скорее всего будет работать нормально вариант
(async () => { const items = [ choosePizza(await getPizzaData()), chosenDrink(await getDrinkData()) ]; [await addPizzaToCart(items[0]), await addDrinkToCart(items[1])] await orderItems() })()
Код исправлен сильно в силу чрезмерной абстрактности.
Пример для одного из предыдущих переводов: repl.it/repls/AmusingZealousEnvironment
Измененный пример для работы с массивами: repl.it/repls/PoliteChocolateHashfunction
Ну и ответ: Ада не видел, видел г@#$%кодеров.iShatokhin
17.04.2018 22:12Скорее всего будет работать нормально вариант
Не совсем. В случае exeption будет Unhandled promise rejection вместо ожидаемого состояния onRejected. Try-catch тоже не сможет поймаль ошибку. Уже обсуждалось:
habrahabr.ru/post/326442/#comment_10175054
apapacy
17.04.2018 22:24async/await это не промисы, не вводите в заблуждение людей переводами таких статей.
Ну как же не промисы. Если промисы. Асинхронная функция возвращает промисы. Всегда.
Оператор await ожидает промис. Об этом совершенно недвусмысленно говорит MDN.
developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Statements/async_function
developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/awaitmayorovp
18.04.2018 09:59Поправка: вот как раз оператор await ожидает не (только) промис, а любой объект с методом then.
apapacy
18.04.2018 16:12Да это реально так. Толкьо что проверил. Но после этого скрипт не работет как ожидается. то есть функция then вяполняется но дальше конечно ничего ожидаемого не происходит. Кто-нибудь значет что-то об этом? Это баг?
try { var a = {}; a.then=function(){console.log('then'); return 'not promise';}; a.catch=function(){console.log('catch'); return 'not promise';}; async function test() { await a; } Promise.resolve().then(async function(){ var a = await test(); console.log('a is', a); }); } catch(ex){ console.log('error', ex); }
Запускаем: node test.js
Вывод: then
Почему это так?mayorovp
18.04.2018 16:14Потому что в then передается колбек который она должна вызвать когда настанет время. А вы его игнорируете.
apapacy
18.04.2018 16:46Да с callback работает.
try { var a = {}; a.then=function(resolve, reject){ console.log('then'); resolve('not promise');}; a.catch=function(){console.log('catch'); return 'not promise';}; async function test() { return await a; } Promise.resolve().then(async function(){ var a = await test(); console.log('a is', a); }); } catch(ex){ console.log('error', ex); }
Но это весьма неожиданно т.к. иногда в цепи промисов может оказаться объект и все работет как ожидается. Но если вдруг в его свойствах будет then то все перестанет работать. Конечно then не лучшее незвание для свойства. Но все же. Я это всего лишь имя своства и никакой магии от него не хочестся получать. Если Только это не специально созданный объект Promise. Утиная типизация какая-то получается.faiwer
18.04.2018 19:21Но если вдруг в его свойствах будет then то все перестанет работать
Да, есть такая засада. По сути система работает не с promise-ми а с любыми thenable объектами. И никаких Symbol.thenable для их идентификации не используется. Когда-нибудь с кем-нибудь это может сыграть злую шутку :)
vvadzim
18.04.2018 19:35Утиная типизация какая-то получается.
Эмм… Так утиная типизация — почти как второе имя javascript'a. Все методы массивов работают с объектами, у которых просто есть length. Ну и т.д…
Damaskus
18.04.2018 13:36Не надо так категорично.
async — значит что там может быть промис, а не должен. Это чисто семантическая вещь, которая позволяет писать await внутри, без этого вы получите ошибку компиляции.
Если справа от await будет не Promise — значение просто вернется напрямую.
Он даже нулл прокидывает.mayorovp
18.04.2018 14:27Нет, как раз async означает что функция вернет промис. Это гарантируется по построению.
Damaskus
18.04.2018 18:04Давайте не будем столь категоричны.
В скриптовом языке далеко не все может быть гарантировано.
Вот тут все работает
function testable(x) { if (x > 10) { return new Promise(resolve => setTimeout(()=>resolve(x), 0)); } return x; } async function test() { console.assert(await testable(1) === 1); console.assert(await testable(1000) === 1000); console.assert(await testable(null) === null); } test();
apapacy
18.04.2018 18:14+2Не совсем понял что Вы имеете в виду.
Если функция имеет квалификатор async то она точно вернет промис. Всегда и во всех случаях.
Что имелось в виду подтвердить Вашим примером?
async f(){;}
f() — вернет промис
await f() — вернет undefined
apapacy
17.04.2018 22:43+1Все await в рамках одного выражения отрабатывают параллельно, но никто чего-то этим даже не пытается пользоваться. В итоге мы имеем статьи вида: Чукча не читатель, чукча писатель.
Откуда эта информация? Ваш код рабочий но работает он не параллельно а последовательно. Пусть даже await будет помещен при фактических параметрах.
PaulMaly
18.04.2018 00:57Так много пафоса однако. MDN:
> Цель функций async/await упросить использование promises синхронно и воспроизвести некоторое действие над группой Promises. Точно так же как Promises подобны структурированным callback-ам, async/await подобна комбинации генераторов и promises.
mayorovp
18.04.2018 09:54Все await в рамках одного выражения отрабатывают параллельно, но никто чего-то этим даже не пытается пользоваться.
Вранье. Вот только что попробовал в консоли Хрома:
> const foo = async x => { console.log("begin " + x); await new Promise(resolve => setTimeout(resolve, 100)); console.log("end " + x); return x; } > [await foo(1), await foo(2)] begin 1 end 1 begin 2 end 2 < (2) [1, 2] > await Promise.all([foo(1), foo(2)]) begin 1 begin 2 end 1 end 2 < (2) [1, 2]
normas
18.04.2018 12:04Кто-нибудь может объяснить, почему консоль не выдает ошибку SyntaxError в данном случае?
> [await foo(1), await foo(2)] begin 1 end 1 begin 2 end 2 < (2) [1, 2]
Ведь оператор await используется не в контексте async функции.
Miron11
20.04.2018 12:19-1Все это хорошо, но как решить, особенно в web приложении, кому поручить дождаться промис. Ведь есть соблазн делегировать его ожидание процессу, и тогда скрипач ( await ) не нужен.
И получается то, что у меня творится на web клиенте почты. Которая то повиснет, то не готова выполнять какие — то запросы, после первого запроса. Что же… выполняешь запрос во второй — третий — иногда четвертый раз. По мере того, как их «резиновая» виртуальная машина прогревает под мои запросы каши, они начинают работать.
И получается что пропустив await, его совсем даже не пропустили, а просто передали в руки конечного пользователя. Зато, я почти уверен, все показатели performance dashboard у разработчика пакета ПО зашкаливают.
304
20.04.2018 12:19Псевдосинхронность не добавит нам производительности.
Если мы захотели бы максимально оптимизировать работу программы, то разбили бы выполнение на воркеры(в ноде)
Попробуйте так же поиграться с классическими промисами, будет сложнее — эвэиты читаются проще, что даём нам возможность прикладывать меньше усилий для их группировки/оптимизации.
И, по моему скромному мнению главная проблема async/await — это try/catch hell с бесконечными unhandled promise rejection, который может возникнуть, например, если разработчики не договорились на каком уровне эти ошибки обрабатывать.kashey
20.04.2018 13:09это try/catch hell с бесконечными unhandled promise rejection, который может возникнуть, например, если разработчики не договорились на каком уровне эти ошибки обрабатывать.
Вы хотели сказать — используют любую классическую библиотеку на основе промисов?
devlato
20.04.2018 14:00-1Блин, при всём уважении, вся статья высосана из пальца. Автор бросается громкими словами вроде "проблемы с производительностью!!!", а по факту приведены банальные ошибки программиста, который зачем-то написал код, выполняющий последовательно вещи, которые можно делать параллельно. Проблема с забывчивостью при использовании await — не проблема синтаксической конструкции async/await, а проблема дизайна языка, которая тоже решается довольно просто — используйте TypeScript или другой компилируемый в JavaScript статически типизированный язык.
kalininmr
чтобы избавится от await нужно перестать использовать await.
очень странная идея.
сперва выдумали проблемму(await нужен именно для этого «ада»)
потом просто решили исользовать старые добрые промисы.
Dreyk
а async/await — это и есть старые добрые промисы
и тут мы не отказываемся от await: мы просто используем его по-другому. А чтобы уметь использовать по-другому — см. п. 1 — надо знать, что это просто сахар вокруг промисов.
Хороший перевод хорошей статьи!
kalininmr
я понимаю, что await Promise.all(promises) использует await.
имхо проблемма надуманная.
есть масса мест где последовательные await логичны, да и для того они и придуманны.
AxisPod
Я бы на вашем месте почитал матчасть. async/await != Promise. Именно так. Не так уже пролетал перевод, с таким вот именно пониманием и очень корявыми примерами использования.