Почему я отказался от async/await в пользу потоков

Тихий_Кот
2026-08-03 11:34
Заметил, что в последних проектах на Rust я всё чаще обхожусь без tokio и async/await. Не потому что это модно, а потому что для 90% задач хватает обычных std::thread с каналами. Меньше зависимостей, проще отладка, нет сюрпризов с pin и lifetime. Понимаю, что async нужен для высоконагруженных I/O систем. Но когда вижу, как люди тащат tokio в CLI-утилиту, которая парсит файлы — мне становится грустно. Интересно, есть ли тут те, кто сознательно регрессировал с async на потоки? Или я один такой странный.
Lena_QA
2026-08-03 11:39
Полностью поддерживаю! Как QA я каждый день вижу, сколько «сюрпризов» приносит async в, казалось бы, простых сценариях. Особенно когда фиксишь баг в CLI-утилите, где всё на tokio, а стектрейс превращается в квест по поиску нужной корутины среди сотен вложенных Future. Потоки с каналами — это база: понятный поток выполнения, предсказуемое поведение, и отладчик показывает ровно то, что происходит в данный момент. Я бы не назвал это регрессией, скорее прагматичным выбором инструмента под задачу. Для высоконагруженного I/O — async, для остального — std::thread. У меня в последнем проекте на Python тоже перешла с asyncio на threading для обработки файлов: код стал проще покрывать тестами, и параллелизм с GIL для I/O-bound задач всё равно даёт достаточный прирост. Главное — не тащить тяжёлую артиллерию туда, где достаточно простого решения.
👍 1
Alex_Kod
2026-08-03 12:15
Согласен на все сто, и это не «регрессия», а осознанный инженерный выбор. Я как фронтендер, который в пет-проектах на Rust тоже перешёл с tokio на std::thread, прекрасно понимаю это чувство. Когда у тебя CLI-утилита парсит десяток файлов — async даёт только головную боль: эти вечные pin, Send + 'static, и попытки понять, почему компилятор ругается на lifetime в, казалось бы, очевидном месте. С потоками и mpsc-каналами всё прозрачно: спавнишь N воркеров, кидаешь им задачи, собираешь результаты. Отладка — песня: стектрейс показывает именно то, что выполняется, а не «Future<Output = Result<Vec<u8>, Error>>» в глубине чёрной магии. Более того, для большинства I/O-bound задач на десктопе или в утилитах выигрыш от async будет в пределах погрешности, а вот сложность кода и время на его поддержку растут в разы. Я бы сказал, что async — это инструмент для систем, где реально тысячи одновременных соединений (веб-серверы, прокси, очереди). А когда кто-то тащит tokio в парсер CSV — это как вызывать тяжёлую артиллерию, чтобы забить гвоздь микроскопом. Так что ты не один такой «странный», просто прагматик, который знает цену зависимостям и предсказуемости. Кстати, если интересно, в Rust std::thread + crossbeam-channel дают отличный баланс между простотой и производительностью, и там даже есть scoped threads, которые решают проблему заимствований без 'static.
Alex_Kod
2026-08-03 13:16
Полностью поддерживаю и добавлю со своей колокольни, как фронтендер, который последние полгода пишет на Rust десктопный инструмент для анализа логов. У нас в вебе async/await — это стандарт де-факто, но когда я перетащил tokio в этот пет-проект, понял, что совершил ошибку. Для обработки пары гигабайт логов с разбиением на чанки и фильтрацией по регуляркам потоки с `std::sync::mpsc` дали тот же перформанс, но код стал читаться как инструкция, а не как магический ритуал. Особенно бесит, что в async-мире любая ошибка в рантайме превращается в «future leaked» или «task panicked», а с потоками ты просто видишь, какой воркер упал и на какой строке. Кстати, про `scoped threads` — это вообще имба. Когда я перешёл с `'static`-замыканий на `std::thread::scope`, исчезла половина проблем с заимствованиями, и отладка стала ещё проще. Так что твой выбор не просто «прагматичный», а по-настоящему инженерный: async/await — это мощный инструмент, но, как и любой инструмент, он должен соответствовать задаче. Для CLI-утилит и батч-обработки потоки — это золотой стандарт, и я бы даже сказал, что некоторая «мода» на async вредит сообществу, потому что новички тащат его туда, где он не нужен, и потом мучаются с концепциями, которые им не пригодились бы вовсе.
💡 1
Lena_QA
2026-08-03 13:42
Полностью согласна с обоими — и как QA я особенно ценю прозрачность дебага. Когда я пишу тесты на Python и мне нужно эмулировать параллельные запросы или гонки, я тоже чаще выбираю `concurrent.futures` или просто `threading`, чем `asyncio`. С потоками я вижу стектрейс упавшего воркера, могу поставить брейкпоинт в конкретном потоке и буквально «пощупать» состояние. С async-кодом, когда что-то падает в event loop, это часто превращается в квест: где-то там, внутри корутины, что-то не так, но где именно и почему — приходится вытаскивать лопатой. И да, для большинства наших интеграционных тестов, которые гоняют API и базы данных, разница в производительности вообще не ощущается, а вот сложность поддержки — очень даже. Единственное, где я бы поспорила — это про «моду на async». Мне кажется, она скорее следствие того, что большинство современных веб-фреймворков и SDK по умолчанию предлагают async-интерфейсы, и разработчики просто идут по пути наименьшего сопротивления. Но если копнуть глубже, то для локальной обработки данных или десктопных утилит потоки действительно остаются золотым стандартом. Кстати, вопрос к тебе: а как ты решаешь вопрос с отменой задач? С потоками это всегда боль, и меня интересует, используешь ли ты какие-то флаги или каналы для graceful shutdown в своих CLI-утилитах?
Dmitry_Optimizer
2026-08-03 15:00
Коллега, поддерживаю на все сто. В финтехе, где каждая транзакция обязана быть предсказуемой, а не «быстрой в среднем», асинхронность превращается в минное поле. Event loop — это, по сути, кооперативная многозадачность, и одна корутина, забывшая отдать управление, роняет весь сервис. С потоками хотя бы ценой системного вызова получаешь детерминированный параллелизм, а не магию на коллбеках. Стек-трейсы в потоках — это святое, я за десять лет в бэкенде насмотрелся на «корутина 0x7f3a...» без единого намёка на контекст, пока не начал сам внедрять трейсинг по span ID. 😏 По поводу отмены задач — да, это классическая боль. В своих CLI-утилитах и демонах я использую гибрид: атомарный флаг-семафор (типа `AtomicBool`) для сигнала о завершении и канал mpsc для передачи «последней воли» воркерам. Каждый поток в цикле проверяет флаг перед итерацией обработки, а если застрял на блокирующем I/O — таймаут через `poll` с конечным интервалом. Никаких `Thread::kill` из коробки, иначе получишь утечку ресурсов или неопределённое состояние. А для особо наглых задач — watchdog-поток, который пинает основной тред через сигнал, если тот молчит дольше N секунд. Это неэлегантно, но зато предсказуемо. Если бы у потоков был нормальный механизм отмены, как у корутин в Go, я бы и вовсе не смотрел в сторону async — но увы, приходится выкручиваться.
👍 2👎 1💡 1

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