Асинхронный метод будет действительно асинхронным только в том случае, если он вызывает какой-либо другой асинхронный метод.
Многие разработчики ошибочно полагают, что если они добавят async
перед определением своей функции, вернут промис и используют .then
или await
в этом промисе, их код будет неблокирующим (асинхронным). В ожидании разрешения этого обещания другой код, такой как обработка событий взаимодействия с пользовательским интерфейсом, может выполняться бесплатно.
Эта статья развеет этот миф с помощью нескольких примеров и расскажет вам, когда ваш код действительно асинхронный и неблокирующий.
JavaScript является однопоточным. Он использует только один поток для запуска всего вашего кода. Давайте посмотрим на пример некоторого асинхронного кода:
Найдите минутку и попытайтесь понять, что делает этот код. Мы вызываем асинхронную функцию OrderIceCream()
, которая создаст таймер на семь секунд, а затем выполнит обратный вызов, который зарегистрирует «Ваше мороженое прибыло» и разрешит обещание с сообщением «Мороженое доставлено», которое будет зарегистрировано .then
обратный вызов.
Мы также вызываем функцию EatPizza()
. Мы хотим заказать мороженое и съесть пиццу, пока она в пути, после чего мы примем доставку.
Как вы думаете, каким будет порядок console.logs
? Скопируйте приведенный выше код, вставьте его в консоль браузера (f12) или запустите с помощью Node.
Как мы видим, сначала было записано «Заказ мороженого», а затем «Поедание пиццы». Спустя семь секунд было записано «Ваше мороженое прибыло», а затем «Мороженое доставлено». Это как раз то, что мы хотели!
Пока таймер работал, основной поток JavaScript не блокировался. Можно было свободно выполнять другой код (функция EatPizza()
). Это был пример асинхронного кода.
Теперь давайте посмотрим на другой пример. На этот раз мы немного изменим OrderIceCream()
:
Теперь мы удалили setTimeout()
и вместо этого добавили цикл for, который будет выполняться 10 00 00 00 000 итераций (очень длинный цикл, выполнение которого займет много секунд).
Как вы думаете, какой порядок журналов консоли будет на этот раз? Если возврата промиса и планирования обратного вызова после разрешения этого промиса с помощью .then()
(или использования async-await) было достаточно, чтобы сделать наш код асинхронным и не блокировать основной поток, то мы должны увидеть те же результаты, что и раньше.
Фактический порядок ведения журнала отличается от того, что мы ожидали. Между логами «Заказ мороженого» и «Ваше мороженое прибыло» разрыв в несколько секунд, и мы не хотим ждать, пока прибудет мороженое, чтобы съесть нашу пиццу. Почему это так?
Еще раз помните, что JavaScript является однопоточным. Он использует один поток для запуска всего вашего кода. Это означает, что цикл for также будет выполняться в основном потоке, а остальная часть кода будет выполняться только после завершения цикла for.
Поэтому он будет работать в основном потоке, если у вас есть синхронный код, даже если ваша функция асинхронная.
Обратите внимание, что console.logs
также являются синхронным кодом, но они не требуют много времени для выполнения, поэтому они будут блокировать наш основной поток только на несколько миллисекунд. В течение этого времени, если в вашем пользовательском интерфейсе есть событие нажатия кнопки, обработчик этого события не будет выполняться до тех пор, пока не будет завершено то, что выполняется потоком.
Для простого оператора, такого как console.log
, эта задержка составляет всего несколько миллисекунд, едва заметная для пользователя, но для чего-то вроде большого цикла for, который выполняется в течение 1e10 итераций, пользовательский интерфейс будет заморожен на много секунд, и пользователь заметьте это. Никакой другой код не будет выполняться, пока поток не освободится.
Я использовал пример большого цикла for для имитации длительного синхронного кода, такого как обработка большого набора данных или преобразование длинного видео из одного формата в другой.
Итак, что можно сделать, чтобы этот долго выполняющийся синхронный код не блокировал основной поток? Вы можете использовать «рабочие потоки» для запуска этого. Хотя JavaScript является однопоточным, браузер, который мы используем для запуска кода JavaScript, имеет «рабочие веб-потоки». Точно так же NodeJs также имеет потоки «Node worker». Вы можете запустить большой цикл for (или любой другой продолжительный код) в рабочем потоке. Это заблокирует рабочий поток, но оставит ваш основной поток свободным для выполнения других действий, таких как обработка событий пользовательского интерфейса.
Итак, даже если ваш метод использует промисы и «.then
» или «async-await
», код внутри самого этого метода является синхронным и будет выполняться в одном потоке JavaScript, и если этот код выполняется долго, основной поток не будет выполняться. в состоянии запустить что-нибудь еще в течение длительного времени.
А как насчет setTimeout()
в первом примере? Где таймер считает время? Разве он не блокирует основной поток во время выполнения? Или мы используем рабочие потоки, чтобы наш основной поток был свободен? А как насчет ожидания ответа сети:
В этом примере я создал узел-сервер, который прослушивает localhost:5000 и возвращает ответ (с текстом «Мороженое доставлено» в качестве тела) после пятисекундного ожидания при получении запроса. Итак, как вы думаете, каков будет порядок журналов консоли?
Теперь, когда мы знаем, что весь код выполняется в одном потоке, мы можем подумать, что ожидание ответа сети также произойдет в этом потоке. Это означает, что мы должны сначала увидеть запись «Заказ мороженого», затем будет выполнено ожидание ответа, а через пять секунд мы увидим регистрацию ответа («Доставка мороженого»), за которым следует «Еда». пицца."
Но, как и в первом примере setTimeout()
, мы видим, что и в этом случае функция EatPizza()
может выполняться в ожидании ответа. Сначала регистрируется «Заказ мороженого», затем «Есть пиццу», а через пять секунд регистрируется ответ сервера («Мороженое доставлено»).
Почему так, чтобы таймер и ожидание ответа сети не блокировали основной поток? Блокируют ли они рабочий поток?
Чтобы понять это, вы должны сначала знать, что таймеры и сетевые операции являются операциями ввода-вывода. Как правило, все, что не происходит на ЦП, называется вводом-выводом.
Отправка сетевого запроса (запись запроса в сокет) или чтение ответа (из сокета) — это операции ввода-вывода (так же, как щелчки мыши, нажатия клавиш или чтение/запись жесткого диска). Устройством, выполняющим эти операции ввода-вывода, является сетевая карта. Для подсчета времени используются системные часы.
Библиотечные функции, такие как setTimeout()
и fetch()
, выполняются синхронно в основном потоке (блокируя его на некоторое время), но фактическая работа по подсчету времени, записи на сетевую карту и чтению ответа с сетевой карты — это операции ввода-вывода. .
Типы блокировки потока
Существует два типа блокировки потока:
Блокировка, связанная с ЦП: поток блокируется, поскольку он активно выполняется на ЦП. Второй пример (1e10 итераций цикла for) выше был связан с блокировкой ЦП.
Блокировка привязки ввода-вывода: здесь поток заблокирован, ожидая, когда произойдет событие ввода-вывода. В этом случае поток не выполняется активно на ЦП и находится в спящем режиме (в очереди ожидания в ОЗУ).
JavaScript не блокирует ввод-вывод; он не блокирует поток во время ожидания событий ввода-вывода. Он реализует то, что называется «асинхронным неблокирующим вводом-выводом», что позволяет потоку выполнять другие действия вместо ожидания события ввода-вывода. Вот почему ожидание сетевых ответов или завершения работы таймеров не блокирует поток. Да даже не рабочие темы!
Заключение
Простое использование промисов и .then
или async-await
не сделает ваш код асинхронным (неблокирующим). Весь ваш код выполняется в одном потоке.
Существует два типа блокировки потока:
Блокировка, связанная с процессором: если в ваших асинхронных методах есть длительный синхронный код, он заблокирует основной поток. Чтобы преодолеть эту проблему, вы можете использовать рабочие потоки для запуска длинного синхронного кода, оставляя основной поток свободным для обработки других событий.
Блокировка привязки ввода-вывода: поток блокируется в ожидании события ввода-вывода, такого как щелчок мыши или сетевой запрос/ответ. Чтобы преодолеть это, JavaScript использует асинхронное программирование (асинхронное ожидание, промисы и цикл обработки событий) для реализации асинхронного неблокирующего ввода-вывода. Чтобы понять, как это работает, ознакомьтесь с моей статьей.
В конечном счете, асинхронный метод будет действительно асинхронным (неблокирующим) только в том случае, если он вызывает какой-то другой асинхронный метод (который, в свою очередь, должен вызывать другой асинхронный метод и т. д.). Самый нижний уровень должен быть операцией ввода-вывода (операции ввода-вывода являются чисто асинхронными) или синхронным методом, который выполняется в другом потоке (он блокирует другой поток).
Спасибо за чтение.