В коде ниже компилятор отвергает создание 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());
}
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 в куче:
Box::pin сначала размещает future в куче и возвращает владеющий указатель, который можно перемещать, не перемещая сам объект F. as_mut даёт Pin<&mut F> для вызова poll; сам вызов обычно выполняет runtime, а не пользовательский код.
Закрепление не запускает future, не делает её Send и не гарантирует, что она будет опрошена. Оно только запрещает безопасное перемещение закреплённого объекта. Если конкретный тип реализует Unpin, можно использовать обычный Pin::new; для стекового закрепления применяют специальные механизмы вроде pin!, но закреплённое значение нельзя вернуть наружу после уничтожения соответствующего стека.
Исполнитель хранит набор задач в динамическом контейнере. Если хранить future непосредственно в Vec, расширение вектора может переместить его элементы, что несовместимо с уже выданными закреплёнными ссылками.
Возможны варианты:
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, если это безопасно.