сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Async и tokio

async в трейтах и Pin

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, остальные разбираются в тренажёре.

  1. #rs_async_traits1 / 5
    Зачем в асинхронном коде понадобился Pin?
    A)Чтобы закрепить задачу за конкретным воркером рантайма
    B)Чтобы запретить отмену задачи посреди выполнения
    C)Чтобы разместить future в куче и уменьшить размер стека
    D)Автомат future ссылается на свои поля — его нельзя двигать
    показать ответ и разбор
    +D)Автомат future ссылается на свои поля — его нельзя двигать

    // разбор: После разрезания async fn локальная переменная и ссылка на неё оказываются полями одной структуры. Перемещение такого значения сделало бы внутреннюю ссылку висячей, поэтому poll принимает Pin<&mut Self> — обещание, что значение больше не сдвинется. Отсюда tokio::pin! и Box::pin в местах, где future нужно удержать на месте.

  2. #rs_async_traits2 / 5
    Компилятор требует, чтобы future был Send. Откуда берётся это требование?
    A)Многопоточный рантайм переносит задачи между воркерами
    B)Этого требует трейт Future в стандартной библиотеке
    C)Send нужен значению, живущему дольше одной функции
    D)Send добавляется макросом tokio::main ко всем типам
    показать ответ и разбор
    +A)Многопоточный рантайм переносит задачи между воркерами

    // разбор: Задача, начатая на одном воркере, может продолжиться на другом, поэтому её состояние обязано быть Send — а состояние включает всё, что живёт через await. Типовая причина ошибки — Rc или MutexGuard, удержанные через точку ожидания. Лечится сужением области жизни значения или переходом на current_thread.

  3. #rs_async_traits3 / 5
    Какой тип на самом деле возвращает async fn f() -> u32?
    A)u32, обёрнутый в Poll
    B)Непоименованный тип, реализующий Future<Output = u32>
    C)Готовое значение u32: тип не меняется, меняется способ вызова
    D)Pin<Box<dyn Future<Output = u32>>> — рантайм требует боксирования
    показать ответ и разбор
    +B)Непоименованный тип, реализующий Future<Output = u32>

    // разбор: Компилятор строит анонимную структуру-автомат и объявляет для неё реализацию Future. Именно поэтому у такой функции нельзя записать тип возврата явно, а в трейтах она долго не поддерживалась. Box появляется только там, где future нужно спрятать за трейт-объектом.

  4. #rs_async_traits4 / 5
    Что стоит за записью async fn в трейте с точки зрения типов?
    A)Метод возвращает Box<dyn Future>, который компилятор добавляет неявно
    B)Метод превращается в обычный, а ожидание разворачивается на стороне вызывающего
    C)Метод помечается атрибутом, и рантайм подставляет свою реализацию при вызове
    D)Метод возвращает скрытый ассоциированный тип, реализующий Future
    показать ответ и разбор
    +D)Метод возвращает скрытый ассоциированный тип, реализующий Future

    // разбор: Компилятор разворачивает async fn в трейте в метод, возвращающий impl Future, а под ним — скрытый ассоциированный тип у каждой реализации. Поэтому аллокаций нет и вызов статический. Обратная сторона в том, что у каждой реализации тип свой и неизвестного размера: трейт с таким методом не dyn-совместим, и в Box<dyn Trait> его не положить.

  5. #rs_async_traits5 / 5
    Компилятор ругается, что future не Send из-за Rc внутри асинхронной функции. Какие есть выходы?
    A)Обернуть Rc в Mutex: блокировка сделает его пригодным для передачи между воркерами
    B)Заменить Rc на Arc либо сузить его область жизни так, чтобы он не пересекал точку ожидания
    C)Пометить функцию unsafe: это снимает проверку авто-трейтов для её состояния
    D)Перенести Rc в отдельную задачу и обращаться к нему через канал сообщений между ними
    показать ответ и разбор
    +B)Заменить Rc на Arc либо сузить его область жизни так, чтобы он не пересекал точку ожидания

    // разбор: В состояние задачи попадает всё, что живёт через await, поэтому Rc, созданный и уничтоженный между двумя точками ожидания, проблемой не является — часто достаточно передвинуть его или обернуть кусок в блок. Если значение должно жить через ожидание, берут Arc. Мьютекс здесь не спасает: неатомарным остаётся сам счётчик ссылок Rc.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.