Теория языков программирования
Async/await и корутины
Node.js обслуживает 1 миллион соединений на одном процессе. Rust tokio - миллионы concurrent requests на нескольких CPU. Всё это на корутинах и cooperative scheduling. Один паттерн, революционный результат.
- **Discord**: перешёл с Python asyncio на Rust tokio для голосовых серверов - обрабатывает 1M+ голосовых соединений с задержкой <10ms
- **AWS Lambda SnapStart**: Java Loom virtual threads позволяют использовать blocking IO без overhead - упрощает serverless код без потери масштабируемости
- **Android Kotlin coroutines**: Google рекомендует coroutines как primary async solution - заменили RxJava в большинстве Android проектов
Корутины
Корутина - функция, выполнение которой можно приостановить и возобновить позже. В отличие от потока, приостановка корутины кооперативная (явный yield/await), а не вытесняющая (планировщик ОС). Одна корутина занимает ~1KB стека vs ~1MB для OS thread.
Kotlin coroutines компилируются в state machines - нет overhead от OS threads. 100k корутин vs 100k threads: RAM в ~1000x меньше, нет context switch overhead (~1-10μs на переключение).
В чём принципиальное отличие корутины от OS thread?
Async как State Machine
Компилятор трансформирует async функцию в state machine. Каждый `await` - переход между состояниями. Функция 'вспоминает' где остановилась через enum с полями - локальные переменные сохраняются между вызовами.
Почему async функция в Rust не требует отдельного OS thread?
Cooperative Scheduling
Cooperative scheduling - корутины явно уступают управление через await/yield. Runtime (executor/event loop) запускает другие корутины пока текущая ждёт IO. Node.js event loop, tokio (Rust), asyncio (Python) - реализации cooperative scheduling.
Почему CPU-intensive задачи опасны в cooperative scheduling?
Structured Concurrency
Structured concurrency - принцип: дочерние задачи не переживают родительский scope. Если родитель завершается/бросает исключение - все дочерние отменяются. Kotlin coroutineScope, Swift async let, Java Loom StructuredTaskScope.
async/await решает все проблемы concurrency - можно просто добавить async к каждой функции
async/await решает IO concurrency. CPU-bound код, shared mutable state, race conditions - требуют дополнительных механизмов
async/await = cooperative scheduling для IO waiting. CPU-bound -> worker pool. Shared state -> Mutex/Channel. Отменять задачи -> structured concurrency + cancellation. Каждая проблема требует своего инструмента
Что гарантирует structured concurrency?
Итоги
- **Корутины**: ~1KB стека, cooperative suspension через await. 100K корутин vs 100K threads: в 1000x меньше RAM
- **State machine**: компилятор трансформирует async функцию. Каждый await = переход состояния. Локальные переменные сохраняются в enum
- **Cooperative scheduling**: await = точка уступки. CPU-bound код без await блокирует event loop - решение через thread pool
- **Structured concurrency**: дочерние задачи не переживают scope. Отмена каскадируется. Нет orphan корутин и утечек
Связанные темы
Async/await связан с более глубокими концепциями:
- Алгебраические эффекты — Async/await - специальный случай algebraic effect handler для Async эффекта
- Модели памяти — Concurrent корутины с shared state требуют знания memory ordering
Вопросы для размышления
- Rust Future<Output=T> - нулевая стоимость без runtime. Go goroutines - встроенный runtime с scheduler. Какой подход лучше и почему оба популярны?
- Structured concurrency решает проблему orphan tasks. Как она соотносится с RAII в Rust - оба гарантируют cleanup при выходе из scope?
- JavaScript однопоточный, но Node.js обслуживает тысячи concurrent requests. Объясните это не-программисту через аналогию из реального мира.