Pin
- очень запутанная тема, с которой я столкнулся при программировании в Rust. Я старался научиться этому, но руководства, статьи и видео, которые я смотрел, трудно понять. Они обычно связаны с необходимостью знать другую сложную концепцию в Rust, и это заставило меня ходить между статьями, видео и руководствами по другим концепциям.
В этой статье я отфильтрую для Вас все остальные понятия и сосредоточусь исключительно на pin
. Прочитав эту статью, вы научитесь применять pin
в коде и понимать его использование в других исходниках.
Что такое pin
Pin
является важной особенностью в Rust. Он позволяет разработчикам прикреплять объект к позиции в памяти, чтобы ваши данные не могли переместиться ещё куда-либо.
Эта функция необходима при работе с объектами, ссылающимися на другие объекты, которые имеют тенденцию изменять свое положение в памяти, независимо от того, необходимо это или нет. При построении структур данных, таких как связанные списки или работа с асинхронным кодом, это может повлиять на код и вызвать неопределенное поведение.
Как закрепить объект в памяти?
Закреплять объект можно довольно легко. Rust предоставляет структуру Pin
, позволяющую закреплять объекты. Структура является частью стандартной библиотеки Rust, доступ к нему можно получить через std::pin::Pin.
Рассмотрим следующий пример:
struct MyStruct {
value: u32
}
fn main() {
let my_struct = MyStruct{ value: 10 };
println!("{}", my_struct.value);
}
Этот пример содержит простую структуру, которая используется в main
функции. В main
, мы создали экземпляр структуры с value
10, а затем вывели на печать это значение в консоль.
Мы можем закрепить my_struct
, Box::pin()
. И это сделаем в коде ниже.
use std::pin::Pin;
struct MyStruct {
value: u32,
_pin: PhantomPinned,
}
fn main() {
let mut my_struct: Pin<Box<MyStruct>> = Box::pin(MyStruct {
value: 10,
_pin: PhantomPinned,
});
println!("{}", my_struct.value);
}
Я не говорил о Box
, так что давайте разберемся. Box
- это структура, позволяющая выделять память в куче.
Rust имеет два типа памяти, которые он использует для хранения значений в коде: стек и куча. Rust хранит данные заданного размера в стеке и данные размеры которых определяются во время выполнения в куче. Память стека имеет ограниченные возможности хранения, но быстрее, чем память в куче. Но память в куче более обширна и гибка, чем память стека.
Box::pin()
выделяет память в куче и устанавливает данные на постоянное место.
Следующий список содержит сведения, необходимые для закрепления значений:
- Модификация в
pin
объект - Свойство
_pin
- Какой риск в использовании
pin
объектов
Давайте рассмотрим все это в деталях!
Изменение данных закрепленного объекта
Одна из вещей, в которой вы застрянете сразу после закрепления объекта - это попытка изменить данные в нем. Это может расстроить, но не бойтесь. Есть способ обойти это, но он включает в себя некоторые процессы нарушения правил. Мы будем выполнять эти процессы в unsafe
блоке.
Давайте вернемся к последнему примеру, который был у нас в предыдущем разделе.
Допустим, мы хотим изменить значение my_struct.value
на 32. Если мы просто попытаемся сделать my_struct.value = 32
, компилятор выдаст сообщение о ошибке, сообщающее, что это не сработает.
Чтобы изменить значение my_struct.value
, необходимо выполнить несколько шагов:
- Сначала сделайте изменяемой ссылку на закрепленный объект с помощью
Pin::as_mut(&mut my_struct)
. - Затем используйте эту изменяемую ссылку для ссылки на объект, с помощью
Pin::get_unchecked_mut(mut_ref)
. - Наконец, используйте ссылку на объект для изменения объекта по своему усмотрению.
Все внесенные изменения будут отражены в закрепленном объекте.
Посмотрим, как выглядит код после выполнения предыдущих шагов.
use std::pin::Pin;
struct MyStruct {
value: u32,
_pin: PhantomPinned,
}
fn main() {
let mut my_struct: Pin<Box<MyStruct>> = Box::pin(MyStruct {
value: 10,
_pin: PhantomPinned,
});
println!("{}", my_struct.value);
unsafe {
let mut_ref: Pin<&mut MyStruct> = Pin::as_mut(&mut my_struct);
let mut_pinned: &mut MyStruct = Pin::get_unchecked_mut(mut_ref);
mut_pinned.value = 32;
}
println!("{}", my_struct.value);
}
При выполнении кода отображается значение my_struct.value
в терминале до и после его изменения.
Свойство _pin
Если вы заметили, мы добавили свойство _pin
в структуру при внесении изменений в наш код. Теперь вы можете спросить себя, что это такое и что оно делает. Это то, что мы рассмотрим в этом разделе.
_pin
- это свойство, помещаемое в структуру, которую требуется закрепить. Оно сообщает компилятору, что структура должна быть закреплена в памяти по постоянному адресу. Метод Box::pin()
можно применить к структуре без свойства _pin
, но он не будет фиксировать ее в памяти.
Вы можете проверить предыдущий это самостоятельно с помощью этого кода:
use std::pin::Pin;
struct MyStruct {
value: u32,
}
fn main() {
let mut my_struct: Pin<Box<MyStruct>> = Box::pin(MyStruct {
value: 10,
});
println!("{}", my_struct.value);
my_struct.value = 32; // без '_pin' это работает без каких-либо проблем
println!("{}", my_struct.value);
}
При использовании свойства _pin
в структуре и инициализации его с помощью PhantomPinned
строка 14 (my_struct.value = 32;
) становится недействительной.
use std::pin::Pin;
struct MyStruct {
value: u32,
_pin: PhantomPinned,
}
fn main() {
let mut my_struct: Pin<Box<MyStruct>> = Box::pin(MyStruct {
value: 10,
_pin: PhantomPinned,
});
println!("{}", my_struct.value);
my_struct.value = 32; // Это приведет к ошибке компиляции
println!("{}", my_struct.value);
}
Если вы посмотрите на свойство _pin
, вы можете спросить, почему необходимо инициализировать его с помощью PhantomPinned
. Ответ прост: PhantomPinned
- это тип, который Rust использует для применения правил закрепления структуры в памяти. При необходимости запрещения релокации структуры в памяти, к свойству _pin
у структуры, всегда следует применять PhantomPinned
. PhantomPinned
не содержит значений в памяти и не выполняет никаких действий, кроме применения правил закрепления структуры в памяти, к которой он применяется.
Чем вы рискуете, используя закрепленные в памяти объекты?
Одна из самых больших проблем с закрепленными объектами - безопасность. Не поймите меня неправильно, закрепление объектов перед их использованием в определенных приложениях способствует безопасности. Теперь вы можете задаться вопросом: я только что сказал, что их проблема в безопасности, что происходит? Проблема безопасности с закрепленными предметами заключается в их использовании.
Возможно, вы заметили, что при изменении закрепленного объекта, my_struct
, пришлось обернуть процессы в unsafe
блок. На это была причина. Как выясняется, вы должны быть предельно осторожны при вмешательстве в закрепленный в памяти объект. Иначе, вы можете вызвать неопределенное поведение и другие проблемы в основных частях вашего кода.
Наш пример был прост, поэтому рисков было не так много. Но все равно нам понадобилось завернуть его в unsafe
блок.
Вывод: зачем тогда закреплять объект в памяти?
Теперь, когда вы прошли через все это, остается последний вопрос. Зачем вообще утруждать себя pin
? Этот вопрос имеет важное значение, поскольку закрепление объектов в памяти по постоянному адресу не будет иметь значения без ответа на них.
Чтобы ответить на эти вопросы, я составил краткий список, в котором говорится о каждой причине:
- Закрепление объекта в Rust гарантирует, что объект останется в фиксированном месте в памяти (жизненно важно для асинхронного программирования).
- Закрепление может помочь предотвратить возникновение гонки данных и других проблем параллелизма при доступе нескольких задач к одним и тем же данным.
- Закрепление также может помочь повысить производительность, сократив копирование и перемещение кода при работе с асинхронными данными.
- Закрепление гарантирует, что определенные типы данных всегда доступны в памяти, даже если компьютер заменяет другие части программы.