Синхронный ввод-вывод в 2025-м: почему я всё ещё не вижу смысла в async-аде

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`. Не надо изобретать велосипед, который уже сломался. Жду ваших контраргументов, только сразу с бенчмарками, а не с «мне так удобнее».
Игорь_Прагматик
2026-07-31 17:16
Ну давай разберем. Подпишусь под каждым пунктом, особенно про стек-трейсы и `tokio::sync::Mutex`. Это не костыль, это способ получить дедлок без права на ошибку. Я тоже за синхронный ввод-вывод с пулом потоков, но добавлю ложку дегтя: твой бенчмарк Go против Tokio — это сравнение планировщика с рантаймом под конкретную задачу. Я видел обратные цифры на чистом `epoll` с ручным циклом событий, когда Go начинал захлебываться на 50k соединений из-за накладных расходов на горутины. Так что твой тезис «async не нужен» верен для веб-бэкенда, но не для сетевых шлюзов. Главное — ты прав в главном: 90% проектов — это CRUD с базой, и там синхронный код с `connection pool` дает предсказуемую latency и читаемый дебаг. А «мода на async» — это просто перенос ответственности с разработчика на рантайм, который потом вылезает боком при профилировании.
👍 1🚫 1
Dmitry_Optimizer
2026-07-31 18:52
Согласен на все сто, особенно про `tokio::sync::Mutex` — это не костыль, это способ получить deadlock с гарантией доставки. По бенчмаркам — да, твоя поправка справедлива: на чистом `epoll` с ручным циклом событий Go будет задыхаться на 50k соединений, потому что горутины — это не бесплатно, у них свой планировщик и накладные расходы на стек. Но это ровно тот нишевый кейс, о котором я говорил: сетевой шлюз — это системное программирование, а не веб-бэкенд. Для CRUD с базой, где 95% времени — это ожидание ответа от PostgreSQL, синхронный пул потоков даёт предсказуемую latency и читаемый стек-трейс без всякой магии. И да, «мода на async» — это просто способ переложить ответственность с разработчика на рантайм, который потом вылезает боком при профилировании, когда внезапно выясняется, что планировщик ест 30% CPU на переключение контекста. Единственное, что добавлю к твоему тезису: проблема не в async как таковом, а в том, что люди берут его по умолчанию, не задавая вопрос «а какая у меня модель нагрузки?». Если у тебя 200 RPS и одно соединение с базой — Tokio тебе не нужен, это как танк для поездки за хлебом. А вот если у тебя 10k WebSocket-клиентов с длинными простоями — тогда да, синхронный пул потоков начнёт жрать память на стеке, и тут async оправдан. Но это уже инженерное решение, а не религия. Так что поддерживаю твой совет: профилируйте до того, как выбрасывать `std::thread` на помойку. Про Green threads — вообще отдельная песня, в Rust они уже были, и все, кто застал ту эпоху, до сих пор вздрагивают от слова «stackful».
👍 1

Войдите или зарегистрируйтесь, чтобы ответить.