Раздел 5 · Асинхронность

Оси асинхронности · поток, ошибки, владение, жадность

middle~25 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Оси асинхронности · поток, ошибки, владение, жадность

В уроке 01-granularity мы ввели карту: любой асинхронный контракт это не одна точка, а набор координат на нескольких осях. Первую ось, гранулярность (сколько значений), мы разобрали тогда. Урок 08-mental-model закрыл ось исполнения (конкурентно или параллельно). Остались четыре оси, по которым контракты различаются на практике. Этот урок проходит их по очереди и сводит в одну матрицу, чтобы под новую задачу ты мог выбирать контракт осознанно, а не по привычке.

Четыре оси на сегодня:

  • flow, кто двигает поток: продюсер пушит или потребитель тянет.
  • propagation, как по цепочке распространяются ошибка и отмена.
  • state-ownership, кто владеет состоянием и какой нужен примитив синхронизации.
  • eagerness, запускается работа сразу или только при подключении.

1. Flow: push против pull

Самая фундаментальная из четырёх. Вопрос простой: кто инициирует следующий шаг.

  • Pull: значение появляется, когда потребитель попросил. Источник пассивен. Итератор, генератор, for await, всё это pull.
  • Push: источник отправляет значение сам, когда оно готово. Потребитель реагирует. addEventListener, EventEmitter, сообщения WebSocket, всё это push.

Покрути переключатель и нажимай шаг. Обрати внимание, кто подсвечивается как инициатор и что происходит с очередью.

В режиме pull очередь никогда не растёт: на каждый next() ровно один шаг, темп задаёт потребитель. В режиме push продюсер эмитит, когда хочет, и если потребитель не успевает забирать, входящая очередь пухнет. Это первый намёк на следующую ось: у голого push нет встроенного тормоза.

В коде разница видна сразу:

// pull: потребитель в темпе, тянет следующее
for await (const page of fetchPages(query)) {
  await render(page); // источник ждёт, пока мы готовы
}

// push: источник в темпе, мы только реагируем
emitter.on('data', (chunk) => {
  buffer.push(chunk); // если render медленный, buffer растёт
});

Чтобы прочитать push-источник циклом, между моделями ставят мост с буфером. Ниже редактор с такой задачей: превратить EventTarget в async iterable и не забыть снять слушателя, когда потребитель ушёл. Тесты гоняются прямо в браузере, ставить ничего не нужно.

2. Propagation: push, pull, hybrid

Ось потока выше была про значения. Эта ось про то, как по той же цепочке распространяются два особых сигнала: ошибка и отмена. И здесь между чистым push и чистым pull появляется третий, гибридный режим.

Чистый pull (async iterable) распространяет ошибку как обычный throw: она летит вверх по for await, останавливает итерацию, попадает в try/catch. Отмена идёт вниз через iterator.return(), когда потребитель вышел по break.

Чистый push (EventTarget) не имеет встроенного канала ошибок вообще. Ошибку приходится отдавать отдельным событием ('error'), и если на него никто не подписан, в Node это роняет процесс. Отмена тоже внешняя: removeEventListener или AbortSignal.

Гибрид, это потоки с обратным давлением. Источник пушит, но обязан остановиться, если потребитель просигналил “буфер полон”. Так работают ReadableStream и Node Stream. Ошибка тут распространяется по конвейеру: упавший этап pipeThrough рвёт всю цепочку и отдаёт ошибку в pipeTo.

// hybrid: источник пушит, но конвейер держит давление
await source
  .pipeThrough(decompress) // упадёт здесь
  .pipeThrough(parse)
  .pipeTo(sink); // ошибка прилетит сюда, цепочка закроется

Практический вывод: выбирая контракт, всегда спрашивай не только “как приходят значения”, но и “куда полетит ошибка и как я отменю это извне”. Push без явного канала ошибок, это будущий ночной инцидент.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.