async в трейтах и Pin
Продвинутый блок: как async уживается с трейтами и зачем вообще нужен Pin. Спрашивают, что изменилось в 1.75, зачем всё ещё нужен async_trait и почему future нельзя двигать.
Стержень: async fn в трейте теперь поддержан языком, но такой трейт не годится для dyn; а Pin существует потому, что автомат future может ссылаться на собственные поля.
// Формулировки: «что изменилось с async в трейтах?», «зачем async_trait сейчас?», «что такое Pin и Unpin?»
Нативный async в трейте
До 1.75 async-методы в трейтах не поддерживались, и все пользовались макросом async_trait, который переписывал сигнатуру в Pin<Box<dyn Future>> - аллокация на каждый вызов. Теперь async fn можно объявить прямо в трейте, и компилятор вернёт непоименованный тип future без всякой кучи.
Ограничения остались. Такой трейт нельзя превратить в трейт-объект: у анонимного типа нет фиксированного размера, положить его в vtable не выйдет. И Send для результата автоматически не добавляется - его приходится требовать отдельно, что важно для многопоточного рантайма.
// Поэтому async_trait жив: он нужен ровно там, где требуется Box<dyn Trait> с асинхронными методами. Платим аллокацией, получаем возможность держать разные реализации в одном списке.
trait Repo {
async fn get(&self, id: u64) -> Option<User>;
}- async в трейте
- метод с async fn прямо в объявлении (1.75+)
- async_trait
- макрос, боксирующий future ради dyn-совместимости
Зачем Pin
После разрезания async fn локальная переменная и ссылка на неё оказываются полями одной структуры-автомата. Такое значение называют самоссылающимся, и перемещать его нельзя: адрес поля изменится, а ссылка внутри останется старой - получится висячая ссылка.
Отсюда Pin: poll принимает Pin<&mut Self>, то есть обещание, что значение больше не сдвинется. Практически это выражается в tokio::pin! для стека и Box::pin для кучи. Типы, которые двигать безопасно, помечены Unpin - таких большинство, и для них Pin ничего не ограничивает.
// Требование Send к future идёт из другого места: многопоточный планировщик может продолжить задачу на другом воркере, поэтому всё живущее через await обязано быть Send. Типовая причина ошибки - Rc или MutexGuard, удержанные через точку ожидания.
- Pin
- обещание, что значение больше не будет перемещено
- Unpin
- маркер типов, которые можно двигать даже под Pin
Как отвечать: «Зачем в асинхронном коде нужен Pin?»
Потому что компилятор превращает async fn в стейт-машину, и локальные переменные, живущие через await, становятся её полями. Если в теле была ссылка на такую переменную, автомат оказывается самоссылающимся: перемещение сдвинуло бы данные, а внутренняя ссылка осталась бы указывать на старый адрес. Pin это обещание, что значение больше не двигают, и именно поэтому poll принимает Pin<&mut Self>. На практике это видно как tokio::pin! для стека и Box::pin для кучи. Типы, которые двигать безопасно, реализуют Unpin, и для них ограничение ничего не значит.
Ответ выводит Pin из устройства стейт-машины, а не заучивает как отдельную сущность. Это ровно та связь, которую хочет услышать интервьюер.
На чём валятся
- −Пробуют сделать Box<dyn Trait> из трейта с нативным async fn.
- −Считают, что async_trait больше не нужен вовсе.
- −Объясняют Pin как способ закрепить задачу за воркером.
- −Держат Rc или guard через await и не понимают ошибку про Send.
- −Не знают про Unpin и думают, что Pin ограничивает все типы одинаково.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Зачем в асинхронном коде понадобился Pin?A)Чтобы закрепить задачу за конкретным воркером рантаймаB)Чтобы запретить отмену задачи посреди выполненияC)Чтобы разместить future в куче и уменьшить размер стекаD)Автомат future ссылается на свои поля — его нельзя двигать
показать ответ и разбор
+D)Автомат future ссылается на свои поля — его нельзя двигать// разбор: После разрезания async fn локальная переменная и ссылка на неё оказываются полями одной структуры. Перемещение такого значения сделало бы внутреннюю ссылку висячей, поэтому poll принимает Pin<&mut Self> — обещание, что значение больше не сдвинется. Отсюда tokio::pin! и Box::pin в местах, где future нужно удержать на месте.
- Компилятор требует, чтобы future был Send. Откуда берётся это требование?A)Многопоточный рантайм переносит задачи между воркерамиB)Этого требует трейт Future в стандартной библиотекеC)Send нужен значению, живущему дольше одной функцииD)Send добавляется макросом tokio::main ко всем типам
показать ответ и разбор
+A)Многопоточный рантайм переносит задачи между воркерами// разбор: Задача, начатая на одном воркере, может продолжиться на другом, поэтому её состояние обязано быть Send — а состояние включает всё, что живёт через await. Типовая причина ошибки — Rc или MutexGuard, удержанные через точку ожидания. Лечится сужением области жизни значения или переходом на current_thread.
- Какой тип на самом деле возвращает async fn f() -> u32?A)u32, обёрнутый в PollB)Непоименованный тип, реализующий Future<Output = u32>C)Готовое значение u32: тип не меняется, меняется способ вызоваD)Pin<Box<dyn Future<Output = u32>>> — рантайм требует боксирования
показать ответ и разбор
+B)Непоименованный тип, реализующий Future<Output = u32>// разбор: Компилятор строит анонимную структуру-автомат и объявляет для неё реализацию Future. Именно поэтому у такой функции нельзя записать тип возврата явно, а в трейтах она долго не поддерживалась. Box появляется только там, где future нужно спрятать за трейт-объектом.
- Что стоит за записью async fn в трейте с точки зрения типов?A)Метод возвращает Box<dyn Future>, который компилятор добавляет неявноB)Метод превращается в обычный, а ожидание разворачивается на стороне вызывающегоC)Метод помечается атрибутом, и рантайм подставляет свою реализацию при вызовеD)Метод возвращает скрытый ассоциированный тип, реализующий Future
показать ответ и разбор
+D)Метод возвращает скрытый ассоциированный тип, реализующий Future// разбор: Компилятор разворачивает async fn в трейте в метод, возвращающий impl Future, а под ним — скрытый ассоциированный тип у каждой реализации. Поэтому аллокаций нет и вызов статический. Обратная сторона в том, что у каждой реализации тип свой и неизвестного размера: трейт с таким методом не dyn-совместим, и в Box<dyn Trait> его не положить.
- Компилятор ругается, что future не Send из-за Rc внутри асинхронной функции. Какие есть выходы?A)Обернуть Rc в Mutex: блокировка сделает его пригодным для передачи между воркерамиB)Заменить Rc на Arc либо сузить его область жизни так, чтобы он не пересекал точку ожиданияC)Пометить функцию unsafe: это снимает проверку авто-трейтов для её состоянияD)Перенести Rc в отдельную задачу и обращаться к нему через канал сообщений между ними
показать ответ и разбор
+B)Заменить Rc на Arc либо сузить его область жизни так, чтобы он не пересекал точку ожидания// разбор: В состояние задачи попадает всё, что живёт через await, поэтому Rc, созданный и уничтоженный между двумя точками ожидания, проблемой не является — часто достаточно передвинуть его или обернуть кусок в блок. Если значение должно жить через ожидание, берут Arc. Мьютекс здесь не спасает: неатомарным остаётся сам счётчик ссылок Rc.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.