Вопросы по async и tokio на собеседовании Rust
Async в Rust живёт по правилам, которых нет в других языках: future не делает ничего, пока его не опросят, отмена происходит молча в точке ожидания, а один блокирующий вызов останавливает целый воркер вместе со всеми его задачами. Спрашивают не синтаксис, а именно эти последствия.
Что спрашивают
- +Future: автомат состояний, poll и Poll::Pending, waker и что будет, если его не сохранить
- +Рантайм: почему исполнителя нет в стандартной библиотеке, executor и реактор, current_thread против многопоточного
- +Задачи: tokio::spawn против await, join! и JoinSet, паника внутри задачи, abort и кооперативная отмена
- +select! и cancel safety: проигравшая ветка уничтожается, какие операции это переживают
- +Ловушки: MutexGuard через await, блокирующий вызов в задаче, безграничный канал и обратное давление
- +async в трейтах с 1.75, зачем нужен Pin, откуда берётся требование Send
Из чего состоит тема
Так тема разложена в тренажёре: движок ведёт прогресс по каждой подтеме отдельно и возвращает те, где вы ошибаетесь.
- async в трейтах и Pin12
- Future и поллинг12
- select! и отмена12
- Задачи: spawn и join12
- Ловушки async12
- Рантайм tokio12
Разборы подтем
Конспект по каждой: что это, как отвечать вслух, на чём валятся, плюс вопросы для самопроверки.
- Рантайм tokio: как устроен асинхронный Rust12 вопросов
- Future и поллинг в Rust12 вопросов
- Задачи в tokio: spawn и join12 вопросов
- select! и отмена в tokio12 вопросов
- async в трейтах и Pin12 вопросов
- Ловушки async в Rust12 вопросов
Примеры вопросов с разбором
- Почему нельзя держать std::sync::MutexGuard через точку await?A)Мьютекс отравится при первом же переключении задачиB)Guard освободится автоматически при await и запись потеряетсяC)Guard не Send: задача не соберётся или задушит остальныхD)Компилятор запрещает блокировки внутри async-функций
показать ответ и разбор
+C)Guard не Send: задача не соберётся или задушит остальных// разбор: Обычный guard привязан к потоку, который взял блокировку, и не Send — задача с ним не собирается для многопоточного рантайма. Но даже там, где соберётся, держать блокировку через ожидание опасно: пока задача ждёт ответа сети, другие стоят в очереди. Правильный путь — сузить область жизни guard или взять асинхронный мьютекс.
- Что изменилось с async fn в трейтах, начиная с версии 1.75?A)Они стали работать в трейт-объектах наравне с обычными методамиB)Они получили автоматическое ограничение Send для возвращаемого futureC)Их можно объявлять прямо в трейте, без внешнего макросаD)Они перестали требовать рантайма для выполнения
показать ответ и разбор
+C)Их можно объявлять прямо в трейте, без внешнего макроса// разбор: Раньше async в трейте разворачивали через async_trait, и каждый вызов стоил Box и аллокации. Теперь компилятор поддерживает такие методы напрямую, возвращая непоименованный тип future. Ограничения остались: dyn-совместимости у таких трейтов нет, а Send для результата приходится требовать отдельно.
- Что произойдёт, если вызвать async-функцию и не написать .await?A)Тело выполнится в фоне, результат будет отброшенB)Функция выполнится синхронно, как обычнаяC)Ничего не выполнится: future никто не опрашиваетD)Рантайм запустит её при первой свободной возможности
показать ответ и разбор
+C)Ничего не выполнится: future никто не опрашивает// разбор: Вызов async fn только строит объект-future и стоит десятки наносекунд: тело не начинает работать, пока его кто-то не опросит. Компилятор напоминает предупреждением must_use про неиспользованный future. Чтобы задача пошла в фон именно как фоновая, её отдают рантайму через spawn — тогда await не нужен сразу.
- Почему для async-кода нужен внешний рантайм вроде tokio?A)Язык даёт синтаксис и трейт Future, но исполнителя в std нетB)tokio нужен только для сети: без ввода-вывода хватает stdC)Стандартный рантайм есть, но он однопоточный и годится лишь для тестовD)Рантайм требуется, чтобы компилятор мог разрезать async fn на состояния
показать ответ и разбор
+A)Язык даёт синтаксис и трейт Future, но исполнителя в std нет// разбор: В стандартной библиотеке есть Future, Poll и Waker — контракт. Кто и как опрашивает задачи, следит за таймерами и сокетами, решает рантайм: tokio, smol, async-std. Это осознанное решение: серверу нужен многопоточный планировщик, встраиваемой системе — крошечный исполнитель без ОС.
- Что делает макрос select!?A)Ждёт первую готовую ветку, остальные бросает незавершённымиB)Выбирает ветку по приоритету, заданному порядком объявленияC)Дожидается всех веток и возвращает их результаты кортежемD)Запускает ветки в разных задачах и собирает результаты
показать ответ и разбор
+A)Ждёт первую готовую ветку, остальные бросает незавершёнными// разбор: Классический приём — гонка операции с таймером: кто первым готов, тот и выиграл. Проигравшие future уничтожаются прямо в точке ожидания, то есть отменяются. Отсюда главный вопрос при использовании: безопасно ли прерывать проигравшую ветку в произвольной точке.
- Чем tokio::spawn отличается от простого .await на том же future?A)spawn выполняет future синхронно и сразу возвращает результатB)spawn отдаёт задачу рантайму, и она идёт конкурентно с текущейC)spawn ставит задачу в очередь и запускает после завершения текущейD)spawn создаёт системный поток под каждую задачу
показать ответ и разбор
+B)spawn отдаёт задачу рантайму, и она идёт конкурентно с текущей// разбор: await исполняет future прямо в текущей задаче: две операции по 50 мс подряд дадут 100 мс. spawn регистрирует задачу в рантайме, и она крутится параллельно, а JoinHandle позволяет забрать результат позже — те же две операции займут 50 мс. Плата — требование 'static и Send для передаваемого future.
- Когда tokio::sync::Mutex выгоднее std::sync::Mutex?A)Когда конкуренция высокая: асинхронный мьютекс быстрееB)В асинхронном коде обычный мьютекс не работаетC)Когда защищаемые данные не реализуют SendD)Когда блокировку нужно удерживать через await
показать ответ и разбор
+D)Когда блокировку нужно удерживать через await// разбор: Асинхронный мьютекс дороже: ожидание идёт через планировщик, а не через примитив ОС. Его берут ровно тогда, когда блокировку приходится держать через точку ожидания — например, вокруг соединения с базой. Для короткой правки структуры в памяти обычный std-мьютекс проще и быстрее, и это рекомендация самой документации tokio.
- Зачем нужен async_trait, если язык уже умеет async fn в трейтах?A)Он нужен для совместимости со старыми версиями компилятораB)Он боксирует future и делает трейт dyn-совместимымC)Он добавляет ограничение Send автоматически ко всем методамD)Он ускоряет вызовы за счёт статической диспетчеризации
показать ответ и разбор
+B)Он боксирует future и делает трейт dyn-совместимым// разбор: Встроенный async fn возвращает анонимный тип, у которого нет фиксированного размера, — в vtable такой метод не положить. Макрос переписывает сигнатуру в Pin<Box<dyn Future>>, и трейт снова годится для Box<dyn Trait>: платим аллокацией на вызов, получаем возможность держать разные реализации в одном списке.
- Во что компилятор превращает async fn?A)В конечный автомат с состояниями на каждой точке awaitB)В замыкание, вызываемое планировщиком по таймеруC)В генератор, который рантайм исполняет построчно через интерпретаторD)В обычную функцию, которая запускается в отдельном потоке
показать ответ и разбор
+A)В конечный автомат с состояниями на каждой точке await// разбор: Тело разрезается по точкам await: каждая становится состоянием автомата, а локальные переменные, живущие через await, переезжают в структуру состояния. Отсюда и размер future — сотня-другая байт для простой функции, — и требование Pin: у автомата бывают ссылки на собственные поля.
это 9 из 72
Ещё 63 вопросов по теме — в тренажёре, с движком повторения
Прочитать разбор и ответить самому — разные навыки. В Сеньорчике вопросы идут сессиями, а движок возвращает подтемы, где вы ошибаетесь, пока они не начнут отскакивать. Бесплатно, лимит по энергии.
Частые вопросы
Что произойдёт, если вызвать async-функцию без await?
Ничего: вызов лишь построит future — структуру-автомат. Тело начнёт выполняться, только когда future кто-то опросит, а компилятор ограничится предупреждением must_use про неиспользованное значение.
Почему нельзя звать std::thread::sleep внутри задачи?
Он усыпляет поток целиком вместе со всеми задачами, которые на нём крутятся: планирование в tokio кооперативное, и пока задача не дошла до await, воркер принадлежит ей. Для сна есть tokio::time::sleep, для тяжёлой синхронной работы — spawn_blocking.
Что такое cancel safety?
Свойство операции переживать уничтожение в точке ожидания без потери данных и порчи инварианта. Оно важно в select!: проигравшая ветка отменяется молча, и если она успела прочитать половину сообщения, байты пропадут.