Программирование RustКонкурентность и asyncРазработчик Rust, занимающийся асинхронными сервисами

В коде ниже компилятор отвергает создание Pin. Какое свойство async future делает закрепление в памяти необ...

В коде ниже компилятор отвергает создание Pin<&mut F>. Какое свойство async-future делает закрепление в памяти необходимым перед передачей future исполнителю?

use std::future::Future;
use std::pin::Pin;

async fn task() {
    let text = String::from("data");
    async {}.await;
    println!("{text}");
}

fn pollable<F: Future>(mut future: F) {
    let pinned = Pin::new(&mut future);
    let _ = pinned;
}

fn main() {
    pollable(task());
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Pin нужен потому, что future после преобразования async может содержать состояние, адрес которого нельзя менять между вызовами poll. Pin::new(&mut future) безопасен только для типа, реализующего Unpin, а произвольный F: Future этого не гарантирует. Поэтому future обычно передают исполнителю как Pin<Box<F>> или получают закреплённую ссылку другим безопасным способом.

Исторический контекст

async/await компилируется в конечный автомат: локальные переменные, живущие через .await, становятся полями его состояния. Для поддержки таких состояний нужен контракт, позволяющий исполнителю приостанавливать future и возобновлять её позже.

Часть future может содержать ссылки на собственные поля или иным образом зависеть от стабильного адреса в памяти. Протокол Future::poll поэтому принимает Pin<&mut Self>, а не обычный &mut Self: это отделяет возможность изменять состояние future от возможности перемещать сам объект.

Постановка проблемы

В примере функция pollable принимает любой F: Future, но пытается создать Pin<&mut F> через Pin::new. Этот конструктор требует F: Unpin, поскольку обычный &mut F не доказывает, что объект уже закреплён и не будет перемещён.

Если без такого ограничения разрешить произвольное перемещение future после начала её выполнения, внутренние ссылки или другие адресные инварианты могли бы указывать на прежнее место. Это привело бы к нарушению гарантий памяти, поэтому безопасный Rust останавливает ошибку на этапе компиляции.

Подробное решение

Unpin означает, что значение типа можно безопасно перемещать даже через Pin. Это не означает потокобезопасность и не связано с Send или Sync; это исключительно гарантия о допустимости перемещения в памяти.

Исправленный вариант обычно закрепляет future в куче:

use std::future::Future; use std::pin::Pin; async fn task() {} fn pollable<F: Future>(future: F) { let mut future: Pin<Box<F>> = Box::pin(future); let _ = future.as_mut(); } fn main() { pollable(task()); }

Box::pin сначала размещает future в куче и возвращает владеющий указатель, который можно перемещать, не перемещая сам объект F. as_mut даёт Pin<&mut F> для вызова poll; сам вызов обычно выполняет runtime, а не пользовательский код.

Закрепление не запускает future, не делает её Send и не гарантирует, что она будет опрошена. Оно только запрещает безопасное перемещение закреплённого объекта. Если конкретный тип реализует Unpin, можно использовать обычный Pin::new; для стекового закрепления применяют специальные механизмы вроде pin!, но закреплённое значение нельзя вернуть наружу после уничтожения соответствующего стека.

Ситуация из практики

Исполнитель хранит набор задач в динамическом контейнере. Если хранить future непосредственно в Vec, расширение вектора может переместить его элементы, что несовместимо с уже выданными закреплёнными ссылками.

Возможны варианты:

  • хранить future в Box и перемещать только указатели; это требует отдельных выделений памяти, но даёт стабильный адрес;
  • заранее резервировать место в Vec; это не является надёжной общей гарантией, поскольку дальнейшее изменение ёмкости снова может вызвать перемещение;
  • использовать unsafe для ручного управления адресами; это уменьшает накладные расходы лишь в специальных случаях, но перекладывает на разработчика доказательство корректности.

Обычно выбирают Pin<Box<dyn Future<Output = ()> + Send>> или эквивалентный конкретный тип: стоимость выделения памяти принимается ради простого и проверяемого контракта. Результат — runtime может безопасно хранить задачи, перемещать их дескрипторы и передавать закреплённые ссылки в poll.

Что кандидаты часто упускают

1. Перемещается ли объект, если переместить Pin<Box<T>>?

Нет. При перемещении Pin<Box<T>> перемещается сам указатель-владелец, а объект T остаётся по тому же адресу в куче. Именно поэтому такой тип подходит для хранения в контейнере, который может перераспределять свои элементы.

Это отличается от Pin<&mut T>: такая ссылка не владеет объектом и действительна только пока живёт исходное закрепление. Само наличие Pin не продлевает время жизни значения.

2. Делает ли Pin future потокобезопасной?

Нет. Pin отвечает только за неподвижность объекта. Для передачи future между потоками ей нужен Send, а для безопасного совместного доступа — соответствующие гарантии Sync; эти свойства проверяются независимо.

Например, future может быть корректно закреплена в куче, но содержать Rc и поэтому не быть Send. Такой объект нельзя передать многопоточному runtime, несмотря на наличие Pin<Box<_>>.

3. Почему Future::poll принимает Pin<&mut Self>, если не каждая future самоссылочная?

Потому что интерфейс должен быть единым для всех реализаций и безопасно поддерживать future, которым стабильный адрес необходим. Тип, которому закрепление фактически не нужно, может реализовать Unpin; тогда Pin<&mut T> можно безопасно получить из обычной &mut T.

Такой дизайн позволяет runtime не анализировать внутреннее устройство каждой future. Он всегда соблюдает общий контракт poll, а конкретный тип сам предоставляет более слабое требование через Unpin, если это безопасно.