Dmitry_Optimizer
2026-07-31 17:05
Тема, от которой у половины зала дёргается глаз, но я рискну. Десять лет назад я ушёл из Java в Go именно из-за того, что async-подход в JVM превращал код в слоёный пирог из callback'ов и фьючерсов. Прошло время, я уже два года пишу на Rust, и каждый раз, когда вижу очередной `tokio::spawn` в хот-пате, меня пробивает на сарказм. Давайте честно: для 90% сервисов, которые мы пишем, синхронный ввод-вывод с пулом потоков — это не просто достаточно, это оптимально. У меня есть цифры, и они не в пользу async.
Начну с главного мифа: «async быстрее». Это не так, если речь о задержках (latency) и пропускной способности (throughput) в реалистичных сценариях. Я недавно переписал один внутренний API на Go — чистый net/http с `sync.Pool` для буферов — и сравнил с аналогичным решением на Tokio. Под нагрузкой 10k RPS, 90-й перцентиль был лучше на 12% у Go, а 99-й — на 20%. При этом потребление памяти у Go было ниже в 1.5 раза. Асинхронность даёт выигрыш только когда у вас тысячи одновременных соединений с длинными простоями (например, WebSocket или long-poll), но это нишевая история.
Второй момент — сложность отладки. Синхронный код — это стек-трейс, который читается как книга. Асинхронный — это простыня из `poll` и `Future::wake`, где понять, где упало, можно только с трассировкой. Я помню, как в одном финтех-проекте мы три дня искали дедлок в Tokio, потому что `Mutex` в async-контексте — это отдельный вид извращения. В синхронном коде такой проблемы нет: вы просто берёте `std::sync::Mutex` и живёте спокойно. И не надо мне рассказывать про `tokio::sync::Mutex` — это костыль, который убивает параллелизм.
Третий пункт — масштабируемость команды. У меня был опыт, когда в проект на Node.js (async-адище) пришёл новый разработчик, и он две недели не мог понять, почему `await` в цикле не работает так, как ожидается. В синхронном коде порог входа ниже на порядок. Я не говорю, что async — это зло, но если у вас нет реальной проблемы с C10k, вы платите за сложность без выгоды. В Rust это особенно заметно: borrow checker + async — это комбинация, которая выбивает из колеи даже опытных людей.
Теперь о том, где async реально нужен. Файловый ввод-вывод в Windows — да, там без `Overlapped` никак. Высоконагруженные сетевые шлюзы с десятками тысяч соединений — окей, берите `io_uring` или `epoll`, но это уже системное программирование, а не веб-бэкенд. В моём pet-проекте на Rust я делаю TCP-сервер для телеметрии, и я сознательно отказался от Tokio в пользу `std::thread` + `mpsc`. Канал на 10Gbps, 20 потоков, каждый крутит `recv` в цикле — и это работает стабильно уже полгода. Ни одного дедлока, ни одного утечки.
Почему же все бросаются в async? Я вижу три причины. Первая — мода: все пишут, все хвалят, и если ты не async, значит ты отстал. Вторая — непонимание модели: люди думают, что async автоматически ускорит их код, но на деле они просто переносят нагрузку с ядер на планировщик. Третья — экосистема: библиотеки уже написаны под async, и проще присоединиться, чем спорить. Но это слабые аргументы, когда речь о production-системах.
Мой совет: прежде чем брать async, сделайте профилирование. Если у вас 95% времени уходит на ожидание I/O, и соединений больше 5k — тогда да, возможно, async имеет смысл. Во всех остальных случаях — берите синхронный ввод-вывод, пул потоков на количество ядер, и не усложняйте себе жизнь. Я за то, чтобы инструмент соответствовал задаче, а не моде. И да, я знаю, что меня закидают камнями, но я готов аргументировать каждую цифру.
Кстати, если кто-то скажет «а как же Green threads?» — в Rust это уже было, и закончилось всё `std::thread`. Не надо изобретать велосипед, который уже сломался. Жду ваших контраргументов, только сразу с бенчмарками, а не с «мне так удобнее».