Ожидание не обязательно занимает главный поток
Представим небольшой сервис, который показывает список выставок. Обработчик принимает запрос, обращается к хранилищу и позже формирует ответ. При неблокирующем обращении ожидание данных позволяет циклу событий обслуживать другую готовую работу. Когда результат получен, продолжение обработчика тоже должно дождаться своей очереди. Асинхронность описывает организацию выполнения, а не обещание мгновенного результата. Часть операций поддерживается средствами операционной системы, часть использует рабочие потоки среды. Поэтому формулировка о том, что весь Node.js состоит из одного потока, слишком груба. Для разработчика ключевой вопрос проще: какой участок сейчас выполняет JavaScript и сколько времени он удерживает основной поток?
Долгое вычисление остаётся долгим вычислением
Если после получения списка программа сортирует огромный массив или выполняет тяжёлый цикл, процессор занят этой работой. Пока такой участок выполняется в основном потоке, другие обработчики не получают возможность исполняться. Добавление async к функции не переносит цикл в отдельный поток. Аналогично await полезен для ожидания подходящей асинхронной операции, но не превращает уже выполняющийся синхронный расчёт в фоновый. Для тяжёлых вычислений рассматривают уменьшение объёма работы, другой алгоритм или выделенное выполнение вне основного потока. Выбор зависит от измерений и требований задачи. Простая замена названия функции создаёт видимость исправления, если причина задержки осталась внутри неё.
Разбирайте путь запроса по участкам
Полезно записать последовательность: принять данные, проверить вход, запросить хранилище, преобразовать ответ, отправить результат. Затем отметить, где происходит ожидание, а где вычисление. Синхронное чтение файла в обработчике и долгая обработка уже прочитанного содержимого создают разные поводы для пересмотра кода. При этом проблема может находиться и за пределами приложения: медленное хранилище увеличивает время ожидания даже при корректной асинхронной организации. Измеряйте длительность отдельных этапов, размеры входных данных и поведение под несколькими одновременными запросами. Не считайте единичный быстрый ответ доказательством устойчивости. В учебном примере достаточно временной схемы; на рабочем сервере изменения проверяют на воспроизводимой нагрузке и с учётом ошибок, ограничений ресурсов и завершения операций.
Попробуйте на практике
Нарисуйте выполнение двух вымышленных запросов к сервису выставок без запуска сервера.
- Назовите запросы А и Б. Для А задайте условные этапы: короткая проверка, ожидание хранилища и короткая подготовка ответа. Для Б оставьте только короткую подготовку ответа.
- Отметьте, что Б поступает во время ожидания данных для А. Покажите на схеме, почему его обработка может начаться до завершения А.
- Измените пример: после получения данных А запускает долгий синхронный расчёт. Поместите поступление Б внутрь этого расчёта и покажите, когда его код сможет выполняться.
- Подпишите два разных направления проверки: сократить или вынести вычисление и исследовать задержку хранилища. Укажите, какое направление относится к каждому варианту.
Как проверить результат. На схеме ожидание данных не смешано с выполнением расчёта. Вы объясняете, почему Б может завершиться раньше А в первом варианте и почему объявление async не освобождает основной поток во втором.
Частые вопросы
Нужно ли запрещать все синхронные операции?
Нет. Контекст важен: короткий отдельный скрипт и сервер, обслуживающий много клиентов, предъявляют разные требования. Особое внимание нужно синхронным операциям на пути обработки запросов, где задержка затрагивает других пользователей.
Несколько запросов обязательно выполняются параллельно?
Нет. Они могут продвигаться по очереди, пока отдельные операции ожидают внешнего результата. Параллельное выполнение и конкурентное обслуживание связаны, но это разные понятия. Для точного ответа нужно смотреть на конкретную операцию и способ её выполнения.
Самостоятельный разбор темы. Содержание конкретной обучающей программы здесь не представлено.