Побег из ада async/await

https://medium.freecodecamp.org/avoiding-the-async-await-hell-c77a0fb71c4c
  • Перевод
Совсем недавно конструкция async/await в JavaScript выглядела как отличное средство для избавления от ада коллбэков. Однако неосмотрительное использование async/await привело к появлению нового ада.


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

  1. Получение списка видов пиццы.
  2. Получение списка напитков.
  3. Выбор пиццы из списка.
  4. Выбор напитка из списка.
  5. Добавление выбранной пиццы в корзину.
  6. Добавление выбранного напитка в корзину.
  7. Оформление заказа.

Выше сделан акцент на том, что операции в скрипте выполняются строго последовательно. Здесь не используются возможности параллельного выполнения кода. Поразмыслим над следующим: почему мы ожидаем получения списка видов пиццы для того, чтобы начать загрузку списка напитков? Следовало бы выполнять эти задачи одновременно. Однако, для того, чтобы получить возможность выбрать пиццу из списка, сначала надо дождаться загрузки списка видов пиццы. То же самое относится и к процессу выбора напитка.

В результате можно прийти к выводу, что задачи, связанные с пиццей, и задачи, связанные с напитками, могут выполняться параллельно, но отдельные операции, относящиеся только к пицце (или только к напиткам) должны выполняться последовательно.

Пример: оформление заказа на основе содержимого корзины


Вот пример кода, в котором осуществляется загрузка данных о содержимом корзины и отправка запроса на формирование заказа:

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?

RUVDS.com

746,91

RUVDS – хостинг VDS/VPS серверов

Поделиться публикацией
Комментарии 53
    +8
    чтобы избавится от await нужно перестать использовать await.

    очень странная идея.
    сперва выдумали проблемму(await нужен именно для этого «ада»)
    потом просто решили исользовать старые добрые промисы.
      +8

      а async/await — это и есть старые добрые промисы


      и тут мы не отказываемся от await: мы просто используем его по-другому. А чтобы уметь использовать по-другому — см. п. 1 — надо знать, что это просто сахар вокруг промисов.


      Хороший перевод хорошей статьи!

        +4
        я понимаю, что await Promise.all(promises) использует await.
        имхо проблемма надуманная.
        есть масса мест где последовательные await логичны, да и для того они и придуманны.
          0
          Даёшь await all [...promises].
          0
          Я бы на вашем месте почитал матчасть. async/await != Promise. Именно так. Не так уже пролетал перевод, с таким вот именно пониманием и очень корявыми примерами использования.
        0

        Мой друзья! Лучше использовать инструменты наподобие map библиотеки Bluebird, которые позволяют управлять количеством одновременно запущенных промисов:


        Bluebird.map(
            array, item => doSomethingAsync(item), {concurrency: LIMIT}
        )

        Это защитит память приложения от переполнения при обработке огромных массивов и предотвратит возникновение блокировок.


        Перевод моего комментария к этой статье.

          +1

          Можно взять маленький модуль p-limit, и не тащить весь большой Bluebird.

            0
            >Это защитит память приложения от переполнения при обработке огромных массивов
            Может сразу Stream api использовать?
            Промисы по своей сути не особо подходят для больших обьемов данных.
              0

              Так-то они тоже теперь на промисы переезжают. Поэтому вполне достаточно использовать Bluebird.map или p-limit, как посоветовали выше.

            +6
            // Задачу можно решить так, как показано выше, но я предпочитаю следующий метод (async () => { Promise.all([selectPizza(), selectDrink()]).then(orderItems) // асинхронный вызов })()

            Разве здесь не нужен await (или убрать фигурные скобки вокруг тела функции)?


            Статья как по мне спорная. Не увидел особого "ада", в основном, начальные примеры показывают, что бывает, если не понимаешь, как работает async/await.


            Как по мне, разобраться с этим уж точно не сложнее, чем с промисами. А код получается на порядки читабельнее.

              +4
              У 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;
              
                +1
                Один try/catch вместо кучи catch это плохо?
                  0
                  Один try/catch вместо кучи catch() — не знаю. Можно обойтись и одним catch()
                  Скорее всего один try/catch это просто само по себе плохо. Т.к. ошибки разные бывают.
                    0
                    кстати да. в яваскрипте с catch довольно плохо, нельзя ловить по типу
                  0
                  Я вот написал библиотеку with-error, оборачивающую функции с исключениями и многоэтажность исчезла. А вообще лучше не пользоваться в JS исключениями, крайне неудобная работа с ними, особенно в области типизации.
                    0
                    Ну во первых, зато нам доступен finnaly.
                    А во вторых вообще претензия не понятно, try catch стандартная конструкция js, которая десятки лет используется для синхронного кода. Если она плоха, то претензия к синтаксису js, более старому чем async/await.
                    +2
                    Последний блок можно написать компактней:

                    const promises = items.map(async (item) => {
                    	await sendRequest(item);
                    });
                    await Promise.all(promises);
                    
                      +3
                      Можно и еще компактней
                      await Promise.all(items.map(sendRequest))
                        0

                        Потеряли return или лишние фигурные скобки.


                        Или бабель добавляет туда return сам? Просто в самой статье тоже была такая же ошибка.

                          0
                          А нужен ли там return?
                          Последняя строчка не ожидает результатов по завершению Promise.all
                            0
                            Автору оригинальной статьи уже сделали замечание по этому поводу и он принес свои извинения. Правда статью не откорректировал пока.
                              0

                              return нужен внутри map, иначе Promise.all ничего не дождётся.

                                0
                                Отсутствие return означает возврат Promise в данном случае.
                                Таким образом в promises попадет массив промисов, которые зарезолвятся в undefined после окончания соответствующих sendRequest.
                                Promise.all дождется факта окончания всех операций, и вернет массив undefined-ов, вместо результатов.
                          +2
                          Зачем при рефакторинге этот вызов делать асинхронной функцией?
                          (async () => {
                          Promise.all([selectPizza(), selectDrink()]).then(orderItems) // асинхронный вызов
                          })()

                          Если уж сделали асинхронной, то почему бы тогда не дождаться резолва Promise.all с помощью await? Promise.all внезано тоже возвращает промис.
                          Сами себе каких-то проблем выдумали, сами их себе порешали.
                            +5
                            Не увидел никакого ада, увидел неоптимальные блокировки.
                              0

                              +1. Вспомнил старый добрый make с его зависимостями, не маскирующимися под императивный код.

                              –3
                              Очередной перевод статьи, автор которой не знает 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

                              Ну и ответ: Ада не видел, видел г@#$%кодеров.
                                0
                                Скорее всего будет работать нормально вариант

                                Не совсем. В случае exeption будет Unhandled promise rejection вместо ожидаемого состояния onRejected. Try-catch тоже не сможет поймаль ошибку. Уже обсуждалось:

                                habrahabr.ru/post/326442/#comment_10175054

                                  0
                                  async/await это не промисы, не вводите в заблуждение людей переводами таких статей.


                                  Ну как же не промисы. Если промисы. Асинхронная функция возвращает промисы. Всегда.
                                  Оператор await ожидает промис. Об этом совершенно недвусмысленно говорит MDN.
                                  developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Statements/async_function
                                  developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/await
                                    0

                                    Поправка: вот как раз оператор await ожидает не (только) промис, а любой объект с методом then.

                                      0
                                      Да это реально так. Толкьо что проверил. Но после этого скрипт не работет как ожидается. то есть функция 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

                                      Почему это так?
                                        0
                                        Потому что в then передается колбек который она должна вызвать когда настанет время. А вы его игнорируете.
                                          0
                                          Да с 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. Утиная типизация какая-то получается.
                                            0
                                            Но если вдруг в его свойствах будет then то все перестанет работать

                                            Да, есть такая засада. По сути система работает не с promise-ми а с любыми thenable объектами. И никаких Symbol.thenable для их идентификации не используется. Когда-нибудь с кем-нибудь это может сыграть злую шутку :)

                                              +1
                                              Утиная типизация какая-то получается.

                                              Эмм… Так утиная типизация — почти как второе имя javascript'a. Все методы массивов работают с объектами, у которых просто есть length. Ну и т.д…
                                        0
                                        Не надо так категорично.
                                        async — значит что там может быть промис, а не должен. Это чисто семантическая вещь, которая позволяет писать await внутри, без этого вы получите ошибку компиляции.
                                        Если справа от await будет не Promise — значение просто вернется напрямую.
                                        Он даже нулл прокидывает.
                                          0
                                          Нет, как раз async означает что функция вернет промис. Это гарантируется по построению.
                                            0
                                            Давайте не будем столь категоричны.
                                            В скриптовом языке далеко не все может быть гарантировано.
                                            Вот тут все работает
                                            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();
                                            

                                              +2
                                              Не совсем понял что Вы имеете в виду.
                                              Если функция имеет квалификатор async то она точно вернет промис. Всегда и во всех случаях.
                                              Что имелось в виду подтвердить Вашим примером?

                                              async f(){;}
                                              f() — вернет промис
                                              await f() — вернет undefined
                                                0
                                                Да, все правильно. Контекст неуловил.
                                        +2
                                        Все await в рамках одного выражения отрабатывают параллельно, но никто чего-то этим даже не пытается пользоваться. В итоге мы имеем статьи вида: Чукча не читатель, чукча писатель.


                                        Откуда эта информация? Ваш код рабочий но работает он не параллельно а последовательно. Пусть даже await будет помещен при фактических параметрах.
                                          +1
                                          Так много пафоса однако. MDN:

                                          > Цель функций async/await упросить использование promises синхронно и воспроизвести некоторое действие над группой Promises. Точно так же как Promises подобны структурированным callback-ам, async/await подобна комбинации генераторов и promises.
                                            0
                                            Все 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]
                                              0
                                              Кто-нибудь может объяснить, почему консоль не выдает ошибку SyntaxError в данном случае?

                                              > [await foo(1), await foo(2)]
                                              begin 1
                                              end 1
                                              begin 2
                                              end 2
                                              < (2) [1, 2]
                                              

                                              Ведь оператор await используется не в контексте async функции.
                                                0
                                                Очевидно, потому что консоль работает «в контексте async функции».
                                            –1
                                            Все это хорошо, но как решить, особенно в web приложении, кому поручить дождаться промис. Ведь есть соблазн делегировать его ожидание процессу, и тогда скрипач ( await ) не нужен.

                                            И получается то, что у меня творится на web клиенте почты. Которая то повиснет, то не готова выполнять какие — то запросы, после первого запроса. Что же… выполняешь запрос во второй — третий — иногда четвертый раз. По мере того, как их «резиновая» виртуальная машина прогревает под мои запросы каши, они начинают работать.

                                            И получается что пропустив await, его совсем даже не пропустили, а просто передали в руки конечного пользователя. Зато, я почти уверен, все показатели performance dashboard у разработчика пакета ПО зашкаливают.
                                              0
                                              Псевдосинхронность не добавит нам производительности.
                                              Если мы захотели бы максимально оптимизировать работу программы, то разбили бы выполнение на воркеры(в ноде)

                                              Попробуйте так же поиграться с классическими промисами, будет сложнее — эвэиты читаются проще, что даём нам возможность прикладывать меньше усилий для их группировки/оптимизации.
                                              И, по моему скромному мнению главная проблема async/await — это try/catch hell с бесконечными unhandled promise rejection, который может возникнуть, например, если разработчики не договорились на каком уровне эти ошибки обрабатывать.
                                                0
                                                это try/catch hell с бесконечными unhandled promise rejection, который может возникнуть, например, если разработчики не договорились на каком уровне эти ошибки обрабатывать.


                                                Вы хотели сказать — используют любую классическую библиотеку на основе промисов?
                                                  +1
                                                  И, по моему скромному мнению главная проблема async/await — это try/catch hell с бесконечными unhandled promise rejection, который может возникнуть, например, если разработчики не договорились на каком уровне эти ошибки обрабатывать.

                                                  Ну если разработчики не договорятся на каком уровне обрабатывать ошибки, то в синхронном коде у них будет такая же проблема, так что это не проблема async/await.
                                                  0
                                                  Блин, при всём уважении, вся статья высосана из пальца. Автор бросается громкими словами вроде "проблемы с производительностью!!!", а по факту приведены банальные ошибки программиста, который зачем-то написал код, выполняющий последовательно вещи, которые можно делать параллельно. Проблема с забывчивостью при использовании await — не проблема синтаксической конструкции async/await, а проблема дизайна языка, которая тоже решается довольно просто — используйте TypeScript или другой компилируемый в JavaScript статически типизированный язык.
                                                    +2
                                                    Вот оже самое хотел написать, статья абсолютно бестолковая. «Беда await ждёт пока завершится асинхронный вызов, не даёт перейти к следующей строке», так в этом этом его суть! Он так и должен работать. Если у кого-то с этим проблемы, то нужно учить его читать спецификацию, а не писать бестолковые жёлтые статьи.
                                                    0
                                                    Суть статьи заключается в том что просто кто то незнает как работает async/await. Подход async/await сильно упростил жизнь а то что есть дураки которые его используют неправильно, так они всегда были есть и будут, с тем же успехом можно взять любую фичу в любом языке найти её неправильное использование, обозвать это адом и сказать никогда ей не пользуйтесь.
                                                      0
                                                      А у автора исходной статьи аноды с катодами синхронность и асинхронность не перепутаны?
                                                      (Как-то так:
                                                      Заголовок спойлера
                                                      When you execute something synchronously, you wait for it to finish before moving on to another task.
                                                      When you execute something asynchronously, you can move on to another task before it finishes.
                                                      )

                                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                      Самое читаемое