Когда async/await
используется в node.js-функции, будет ли он блокировать поток node.js до тех пор, пока он не выполнит следующую строку кода?
Будет ли асинхронный/ожидающий блокировать поток node.js
Ответ 1
async/await
не блокирует весь интерпретатор. node.js по-прежнему запускает весь Javascript как однопоточный, и хотя некоторый код ожидает async/await
ожидающего выполнения, другие события могут по-прежнему запускать свои обработчики событий (поэтому node.js не блокируется). Очередь событий все еще обслуживается для других событий. Фактически, это будет событие, которое разрешает обещание, которое позволит ожидающему перестать await
и выполнит следующий код.
Код как это:
await foo(); // foo is an async function that returns a promise
console.log("hello");
аналогично этому:
foo().then(() => {
console.log("hello");
});
Итак, await
просто помещает следующий код в этой области в невидимый обработчик .then()
а все остальное работает почти так же, как если бы он был написан с помощью обработчика .then()
.
Итак, await
позволяет вам сохранить запись обработчика .then()
и дает к нему синхронный вид кода (хотя на самом деле он не синхронный). В конце это сокращение, которое позволяет писать асинхронный код с меньшим количеством строк кода. Однако нужно помнить, что любое обещание, которое может отклонить, должно иметь попытку/ловить где-то вокруг него, чтобы поймать и обработать это отклонение.
Логически вы можете думать о том, что делает node.js, когда он встречает ключевое слово await
при выполнении функции, следующим образом:
- Вызов функции
- Интерпретатор видит, что функция объявлена как
async
что означает, что она всегда будет возвращать обещание. - Переводчик начинает выполнять функцию.
- Когда он встречает ключевое слово
await
, он приостанавливает дальнейшее выполнение этой функции до тех пор, пока не будет выполнено ожидаемое обещание. - Затем функция возвращает нерешенное обещание.
- На этом этапе интерпретатор продолжает выполнять все, что следует после вызова функции (обычно за
fn().then()
следуют другие строки кода). Обработчики.then()
еще не выполнены, потому что обещание еще не разрешено. - В какой-то момент эта последовательность Javascript заканчивается и возвращает управление обратно интерпретатору.
- Теперь переводчик может обслуживать другие события из очереди событий. Исходный вызов функции, который натолкнулся на ключевое слово
await
все еще приостановлен, но другие события теперь могут быть обработаны. - В какой-то момент в будущем исходное обещание, которое ожидалось, будет решено. Когда пора обработать это в очереди событий, ранее приостановленная функция продолжает выполняться в строке после
await
. Если есть еще операторыawait
, то выполнение функции снова приостанавливается до тех пор, пока не будет выполнено это обещание. - В конце концов, функция выполняет оператор
return
или достигает конца тела функции. Если существует операторreturn xxx
, тоxxx
оценивается, и его результат становится разрешенным значением обещания, которое этаasync
функция уже вернула. Функция теперь выполнена, и обещание, которое она возвратила ранее, разрешено. - Это приведет к тому, что любые обработчики
.then()
прикрепленные к обещанию, которое эта функция ранее возвращала, вызывались. - После запуска этих обработчиков
.then()
работа этойasync
функции наконец выполняется.
Таким образом, хотя весь интерпретатор не блокируется (другие события Javascript все еще могут обслуживаться), выполнение конкретной async
функции, содержащей оператор await
было приостановлено до тех пор, пока обещание, которое ожидалось, не было разрешено. Что важно понять, это шаг 5 выше. Когда достигается первое await
, функция сразу же возвращает неразрешенное обещание и код после выполнения этой функции (до разрешения awaited
обещания). Именно по этой причине весь переводчик не заблокирован. Исполнение продолжается. Только внутренности одной функции приостанавливаются до тех пор, пока обещание не будет разрешено.
Ответ 2
async/await
- это просто синтаксический сахар для вызовов then
по обещанию. Не Promises, а async
и await
создавать новые потоки.
Когда выполняется await
, выражение, которое следует за ним, оценивается синхронно. Это должно быть обещание, но если это не так, оно завернуто в одно, как будто у вас есть await Promise.resolve(expression)
.
После вычисления этого выражения возвращается функция async
- она возвращает обещание. Затем выполнение кода продолжается, когда любой код следует за вызовом функции (тот же поток), пока стек вызовов не будет пустым.
В какой-то момент обещание, которое было оценено для await
, будет разрешено. Это поставит микрозадачу в очередь микрозадач. Когда движок JavaScript не имеет ничего общего с текущей задачей, он будет использовать следующее событие в очереди микрозадач. Поскольку эта микрозадача включает в себя обещанное обещание, она восстановит предыдущее состояние выполнения функции async
и продолжит то, что будет дальше после await
.
Функция может выполнять другие операторы await
с аналогичным поведением, хотя функция теперь больше не возвращается туда, откуда она была изначально вызвана (поскольку этот вызов уже обрабатывался с первым await
), он просто возвращает стек вызовов пуст, и он оставляет механизм JavaScript для обработки микрозадач и очередей задач.
Все это происходит с тем же потоком.
Ответ 3
Пока код, содержащийся внутри async/await, не блокируется, он не будет блокироваться, например, вызовы db, сетевые вызовы, вызовы файловой системы.
Но если код, содержащийся внутри async/await, блокируется, он блокирует весь процесс Node.js, например бесконечные циклы, задачи с интенсивным использованием ЦП, такие как обработка изображений и т.д.
В сущности async/await - это оболочка уровня языка вокруг Promises, так что код может иметь синхронный "внешний вид"
Ответ 4
Будет ли async/await блокировать поток node.js? Как сказал @Nidhin David, это зависит от того, какой код у вас есть внутри асинхронной функции - вызовы БД, сетевые вызовы, вызовы файловой системы не блокируются, но, например, блокирование выполняется долго для циклов /while, JSON stringify/parse и злые/уязвимые регулярные выражения (Google для атак ReDoS).
Этот первый пример блокирует поток главного узла, как и ожидалось, и никакие другие запросы/клиенты не могут быть обслужены.
var http = require('http');
// This regexp takes to long (if your PC runs it fast, try to add some more "a" to the start of string).
// With each "a" added time to complete is always doubled.
// On my PC 27 times of "a" takes 2,5 seconds (when I enter 28 times "a" it takes 5 seconds).
// https://en.wikipedia.org/wiki/ReDoS
function evilRegExp() {
var string = 'aaaaaaaaaaaaaaaaaaaaaaaaaaab';
string.match(/^(a|a)+$/);
}
// Request to http://localhost:8080/ wil be served quickly - without evilRegExp() but request to
// http://localhost:8080/test/ will be slow and will also block any other fast request to http://localhost:8080/
http.createServer(function (req, res) {
console.log("request", req.url);
if (req.url.indexOf('test') != -1) {
console.log('runing evilRegExp()');
evilRegExp();
}
res.write('Done');
res.end();
}).listen(8080);
Вы можете запустить много параллельных запросов на http://localhost: 8080/, и это будет быстро. Затем выполните только один медленный запрос http://localhost: 8080/test/, и никакой другой запрос (даже быстрый на http://localhost: 8080/) не будет обработан, пока медленный (блокирующий) запрос не закончится.
Во втором примере используются обещания, но он все еще блокирует поток главного узла, и никакие другие запросы/клиенты не могут быть обслужены.
var http = require('http');
function evilRegExp() {
return new Promise(resolve => {
var string = 'aaaaaaaaaaaaaaaaaaaaaaaaaaab';
string.match(/^(a|a)+$/);
resolve();
});
}
http.createServer(function (req, res) {
console.log("request", req.url);
if (req.url.indexOf('test') != -1) {
console.log('runing evilRegExp()');
evilRegExp();
}
res.write('Done');
res.end();
}).listen(8080);
В третьем примере используется async + await, но он также блокируется (async + await аналогичен встроенному Promise).
var http = require('http');
async function evilRegExp() {
var string = 'aaaaaaaaaaaaaaaaaaaaaaaaaaab';
string.match(/^(a|a)+$/);
resolve();
}
http.createServer(function (req, res) {
console.log("request", req.url);
if (req.url.indexOf('test') != -1) {
console.log('runing evilRegExp()');
await evilRegExp();
}
res.write('Done');
res.end();
}).listen(8080);
В четвертом примере используется метод setTimeout(), который вызывает медленный запрос, по-видимому, немедленно (браузер быстро получает "Готово"), но он также блокируется, а любые другие быстрые запросы будут ждать до завершения evilRegExp().
var http = require('http');
function evilRegExp() {
var string = 'aaaaaaaaaaaaaaaaaaaaaaaaaaab';
string.match(/^(a|a)+$/);
}
http.createServer(function (req, res) {
console.log("request", req.url);
if (req.url.indexOf('test') != -1) {
console.log('runing evilRegExp()');
setTimeout(function() { evilRegExp(); }, 0);
}
res.write('Done');
res.end();
}).listen(8080);
Ответ 5
У меня только что было "ага!" момент и думал, что я передам это. "await" не возвращает управление напрямую в JavaScript - оно возвращает управление вызывающей стороне. Позвольте мне проиллюстрировать. Вот программа, использующая обратные вызовы:
console.log("begin");
step1(() => console.log("step 1 handled"));
step2(() => console.log("step 2 handled"));
console.log("all steps started");
// ----------------------------------------------
function step1(func) {
console.log("starting step 1");
setTimeout(func, 10000);
} // step1()
// ----------------------------------------------
function step2(func) {
console.log("starting step 2");
setTimeout(func, 5000);
} // step2()
Нам нужно следующее поведение: 1) оба шага запускаются немедленно и 2) когда шаг готов к обработке (представьте запрос Ajax, но здесь мы просто ждем некоторое время), обработка каждого шага происходит немедленно,
Код "обработки" здесь - console.log("шаг X обработан"). Этот код (который в реальном приложении может быть довольно длинным и, возможно, включать вложенные ожидания), находится в режиме обратного вызова, но мы бы предпочли, чтобы он был кодом верхнего уровня в функции.
Вот эквивалентный код с использованием async/await. Обратите внимание, что нам нужно было создать функцию sleep(), поскольку нам нужно ожидать функцию, которая возвращает обещание:
let sleep = ms => new Promise((r, j)=>setTimeout(r, ms));
console.log("begin");
step1();
step2();
console.log("all steps started");
// ----------------------------------------------
async function step1() {
console.log("starting step 1");
await sleep(10000);
console.log("step 1 handled");
} // step1()
// ----------------------------------------------
async function step2() {
console.log("starting step 2");
await sleep(5000);
console.log("step 2 handled");
} // step2()
Важным выводом для меня было то, что ожидание в step1() возвращает управление к основному телу, так что step2() может быть вызвано для начала этого шага, а ожидание в step2() также возвращается к основному коду, чтобы " все шаги начаты "можно распечатать. Некоторые люди рекомендуют использовать "await Promise.all()" для запуска нескольких шагов, а затем обрабатывать все шаги, используя результаты (которые появятся в массиве). Однако, когда вы это сделаете, ни один шаг не будет обработан, пока все шаги не будут решены. Это не идеально, и кажется совершенно ненужным.