← На главную

Tina выбирает предсказуемость thread-per-core вместо async/await

16.07.2026 02:06 · hackernews

async/await победил в войне конкурентности — писать на нём и правда легко. Код выглядит почти как синхронный. Но за знакомым синтаксисом прячется огромная структурная сложность. Рич Хики в своём докладе Simple Made Easy точно сформулировал разницу: «Easy» — это то, что привычно и под рукой, а «Simple» — то, что не запутано внутри. async/await легко писать, но адски сложно эксплуатировать.

Роб Пайк на GopherConAU в 2023 заметил: по сравнению с горутинами, каналами и select, async/await проще и компактнее для разработчиков языков. Но часть сложности перекладывается на программиста — отсюда и пресловутые «цветные функции». И если вы даёте несколько реализаций конкурентности в одном окружении, начинаются проблемы. Именно это сейчас и ломается в продакшене.

Главная ловушка — async/await смешивает асинхронность (ожидание I/O) и конкурентность (работу с кучей задач сразу). Разработчик пишет асинхронную функцию так же, как блокирующийся код: стянул запись из БД по сети, сразу её обработал. Но что будет, если обработка — парсинг 10MB JSON или тяжёлая криптография? Кооперативный исполнитель встанет колом. Поток не отдаст управление, пока не дойдёт до await. Одна задача на 50 миллисекунд — и тысячи других запросов получают дикий лаг. Железо при этом простаивает.

Когда такие всплески случаются, совет один: разделяй рантаймы. I/O на Tokio, тяжёлые вычисления отдавай в пул потоков вроде Rayon. Инженеры PostHog и Meilisearch уже задокументировали, во что это выливается в продакшене. Разработчик вынужден вручную разбирать каждую функцию, решать, куда её определить, и городить обмен сообщениями между двумя разными средами с разными ментальными моделями. Обещание абстракции не выполнено — программист превратился в человека, который вручную планирует задачи.

Вторая проблема: вызов tokio::spawn(...) дёшев, и система не оказывает сопротивления. Когда база данных тормозит, сетевой цикл продолжает принимать соединения и порождать задачи. Память не ограничена по умолчанию. Очереди бесконечно растут, RAM съедается, и процесс убивает OOM killer. Постмортемы крупных платформ показывают одно: очереди не исправляют перегрузку, они просто откладывают крах, делая его катастрофическим.

Кажется, что спасение — work-stealing планировщик, который растаскивает задачи по свободным ядрам. Но на масштабе честность убивает пропускную способность. Когда WhatsApp упирался в лимиты Erlang BEAM на машинах со 100+ ядер, idle-треды тратили все циклы на борьбу за глобальный runq_lock. Перемещение задачи на другое ядро теряет L1 и L2 кэш — каждый такой перенос тянет за собой штраф в 100+ наносекунд на чтение из оперативки. Если тебе всё равно приходится вручную дробить нагрузку, абстрактный планировщик уже проиграл.

Автору надоели эти ловушки. Он хотел отказоустойчивость BEAM, но без сборки мусора и глобального work-stealing. Вместо того чтобы прятать конечный автомат, он решил выставить его наружу и дать пользователю нормальные примитивы управления. Результат — Project Tina: thread-per-core, shared-nothing, без work-stealing и мьютексов, со строгой кэш-локальностью. Нет async, await, Promises или Futures. Разработчик пишет Isolate — обычную синхронную функцию, которая реагирует на сообщение и возвращает Effect. Память выделяется при старте процесса, почтовые ящики строго ограничены — система предсказуемо сбрасывает лишнее, а не падает с OOM. Планировщик — жёсткий однопоточный цикл на ядро. Это даёт детерминированное симулирование: с одним и тем же seed воспроизводится абсолютно тот же порядок выполнения.

async/await делает конкурентность лёгкой для написания, но сложной для эксплуатации. Tina предлагает upfront заплатить за явное управление переходами и памятью, получив взамен структурные гарантии. Предсказуемость бьёт краткость. Исходники — на GitHub.

Читать оригинал →