Внутренняя изменчивость в Rust, часть 3: за кулисами

Перевод | Автор оригинала: Ricardo Martins

Ключевые выводы

Эта статья является частью серии о внутренней изменчивости в Rust. Вы можете прочитать часть 1 здесь и часть 2 здесь.

Введение

В предыдущих статьях мы рассматривали внутреннюю изменчивость в Rust с практической точки зрения, но не исследовали магию, заставившую ее работать. В этой статье мы отдернем занавеску и посмотрим на человека, который прячется за спиной.

Хорошее место для начала - прочитать определение Cell, RefCell, RwLock и Mutex, чтобы собрать некоторые подсказки. К счастью, быстро вырисовывается шаблон: все они содержат поле с общим типом: UnsafeCell.1 Сначала мы исследуем, что это такое, а затем выясним, как оно используется этими типами.

Что такое UnsafeCell?

UnsafeCell определяется в документации как «базовый примитив для внутренней изменчивости в Rust». В самом деле, мы натыкались на это каждый раз, когда смотрели на определение типов, обеспечивающих внутреннюю изменчивость. Однако при всей кажущейся мощи его определение кажется довольно скучным:

#[lang = "unsafe_cell"]
#[stable(feature = "rust1", since = "1.0.0")]
pub struct UnsafeCell<T: ?Sized> {
    value: T,
}

Вот и все? Краеугольный камень внутренней изменчивости выглядит как структура нового типа с некоторыми странностями раньше?

Эти странные строки прямо над определением структуры являются атрибутами компилятора. В основном мы можем игнорировать второй, поскольку он просто отмечает функцию как стабильную, начиная с версии 1.0.0 компилятора, что означает, что ее можно использовать в любом канале (стабильном, бета и ночном). Первый более интересен: это элемент языка, своего рода особый подмигивание и подталкивание к компилятору. Я займусь этим позже.

Помимо обычных методов new() и into_inner(), существует еще метод get():

pub fn get(&self) -> *mut T {
    &self.value as *const T as *mut T
}

Несмотря на то, что это небольшой метод, мы можем сказать, что это несколько необычно: он принимает неизменяемую ссылку на внутреннее значение (&self.value), а затем приводит ее дважды: сначала в необработанный постоянный указатель (* const T), а затем в необработанный изменяемый указатель (* mut T), который возвращается вызывающей стороне.

Необработанные указатели, а? Это ново. Давайте взглянем.

Необработанные указатели

Необработанные указатели похожи на ссылки, к которым мы привыкли. Оба указывают на адрес памяти, но есть важные различия. Ссылки умны: они имеют гарантии безопасности (например, указывают на действительную память и никогда не имеют значения NULL), проверку заимствования и время жизни. С другой стороны, необработанные указатели ... ну, сырые. У них нет ни одной из этих функций, и они просто указывают на адрес памяти, как указатели в C. Самое главное, у них нет гарантий наложения или изменчивости, в отличие от ссылок. Есть два вида необработанных указателей: постоянные (* const T) и изменяемые (* mut T). Разница между ними в том, что мутации не разрешены напрямую для постоянных указателей, но мы можем легко преобразовать их в изменяемые указатели, а затем изменить значение.

Вы могли заметить, что get() не помечен как небезопасный, даже несмотря на то, что мы принудительно превращаем неизменяемую ссылку в необработанный изменяемый указатель и попутно отказываемся от гарантий безопасности Rust. Разве это не должно вызывать тревогу?

Приведение (т. Е. Преобразование) неизменяемой ссылки в необработанный константный указатель - это неплохо. Можно утверждать, что это безопасно, поскольку неизменность ссылки сохраняется, и мы начинаем со ссылки, которая имеет гарантии безопасности, которые мы видели выше, поэтому результирующий необработанный указатель не будет недействительным или нулевым. С другой стороны, переход от необработанного указателя-константы к необработанному изменяемому указателю кажется намного хуже, потому что мы внезапно заявляем, что можно изменять значение, которое не имеет никакого отношения к изменению. Переход оттуда обратно к изменяемой ссылке определенно плох.

Если подумать, эти конверсии сами по себе не опасны, так как сами по себе не имеют побочных эффектов. Фактически он использует (т.е. разыменовывает) необработанные указатели, что потенциально опасно. Таким образом, когда мы пытаемся разыменовать указатель, возвращаемый get(), как в этом примере, компилятор сообщает нам, что мы собираемся сделать что-то потенциально опасное:

error: dereference of raw pointer requires unsafe function or block [E0133]

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

Даже в этом случае изменение неизменяемых данных считается неопределенным поведением:

 Мутирующие неизменяемые данные (то есть данные, полученные через общую ссылку или данные, принадлежащие привязке let), если эти данные не содержатся в UnsafeCell <U>.

Итак, мы вернулись к началу: что делает UnsafeCell особенным?

✨ Волна магии ✨

Помните элемент lang, который предшествовал определению UnsafeCell? И снова вот оно:

#[lang = "unsafe_cell"]

Он вызывает особую обработку компилятором: UnsafeCell получает специальный маркер, который следует за ним на различных этапах компиляции, пока, наконец, не сработает определенный путь кода. Этот раздел кода проверяет наличие этого маркера и, если он присутствует, избегает установки двух атрибутов LLVM, которые применяются к обычным типам: а именно, что значение доступно только для чтения и нет псевдонимов. Эти атрибуты, если они присутствуют, могут допускать некоторую дополнительную оптимизацию с помощью LLVM, которая может мешать желаемому нами поведению UnsafeCell.

Это не означает, что мы абсолютно не можем получить аналогичные результаты без элемента lang - мы можем2, но это чисто случайное совпадение и основано на неопределенном поведении, которое может съесть наше белье, в зависимости от настроения компилятора (т. Е. Сгенерированный код может неожиданно изменится).

Кроме того, обратите внимание, что внутри get() нет синхронизации, что делает UnsafeCell небезопасным для использования несколькими потоками и поэтому помечен! Sync.

Небольшой эксперимент показывает, что использование UnsafeCell не требует затрат времени выполнения после оптимизации компилятора и почти неотличимо от «сырой» переменной, как и ожидалось от абстракций с нулевыми накладными расходами в Rust.

На этом все о UnsafeCell. Это немного удивительно, но этой крошечной структуры с очень простым методом и некоторой поддержкой компилятора достаточно для создания и создания всех других типов внутренней изменчивости, которые мы исследовали ранее. Достаточно немного магии. ✨

Как это используется?

Теперь, когда мы знаем, что такое UnsafeCell, давайте посмотрим, как другие типы основываются на нем.

Клетка

Начнем с Cell, самого простого из всех. Его определение чрезвычайно сжато:

pub struct Cell<T> {
    value: UnsafeCell<T>,
}

Это говорит о том, что Cell просто предоставляет более удобный API для доступа к внутреннему значению, содержащемуся в UnsafeCell. Если мы посмотрим на Cell::get и Cell::set, мы увидим, что UnsafeCell::get() достаточно для реализации обоих методов, и что детали реализации, такие как небезопасные блоки, скрыты от пользователя. :

pub fn get(&self) -> T {
    unsafe{ *self.value.get() }
}

pub fn set(&self, value: T) {
    unsafe {
        *self.value.get() = value;
    }
}

В объявлении impl от Cell есть важная деталь: оно ограничено типами копирования.

impl<T:Copy> Cell<T> {

Без этого ограничения мы могли бы создавать экземпляры Cell вокруг типов копирования, таких как &mut T (изменяемые ссылки), что является ужасной идеей: это приведет к созданию изменяемых ссылок с псевдонимом и нарушению правил псевдонима.

RefCell

RefCell лишь немного сложнее Cell. Он содержит поле UnsafeCell с внутренним значением, как и Cell, и Cell с флагом заимствования для отслеживания состояния заимствования:

pub struct RefCell<T: ?Sized> {
    borrow: Cell<BorrowFlag>,
    value: UnsafeCell<T>,
}

В первой статье этой серии мы видели, что нам нужно вызвать заимствовать или заимствовать_mut в RefCell, прежде чем получить доступ к значению внутри. Оба метода имеют схожие реализации и преобразуют необработанный указатель, полученный от UnsafeCell::get(), в значение обратно в ссылку с помощью &*. Для краткости мы рассмотрим только RefCell::заимствовать(), поскольку RefCell::заимствовать_mut() очень похож.

pub fn borrow(&self) -> Ref<T> {
    match BorrowRef::new(&self.borrow) {
        Some(b) => Ref {
            value: unsafe { &*self.value.get() },
            borrow: b,
        },
        None => panic!("RefCell<T> already mutably borrowed"),
    }
}

Вызов UnsafeCell::get() и обратное преобразование в ссылку. А где же обновление состояния заимствования RefCell?

Чтение BorrowRef::new() проясняет ситуацию. Грубо говоря, когда этот метод вызывается, он проверяет поле заимствования RefCell (которое он получил в качестве аргумента) и обновляет его, если значение можно заимствовать. Поскольку поле заимствования является ячейкой, BorrowRef может изменить его, даже если он получил на него неизменяемую ссылку!

RefCell::заимствовать_mut() очень похож, за исключением того, что он вызывает BorrowRefMut::new() вместо BorrowRef::new() и возвращает изменяемую ссылку (если быть точным, он возвращает RefMut, который реализует DerefMut).

Таким образом, мы можем сделать вывод, что Cell и RefCell - это легкие оболочки вокруг UnsafeCell, предоставляющие нам удобный API для доступа к внутреннему значению UnsafeCell и защищающие нас от опасного разыменования указателей. В случае Cell после оптимизации затраты времени выполнения отсутствуют, в то время как механизм динамической проверки заимствований RefCell вызывает небольшие накладные расходы. Кроме того, Cell и RefCell «заражены» маркером UnsafeCell! Sync.

RwLock и Mutex

В отличие от Cell и RefCell, RwLock и Mutex представляют собой более сложные структуры данных, которые обеспечивают синхронизированный доступ к внутреннему значению, что позволяет им реализовать Sync и соблюдать эту гарантию. Мы не будем углубляться в них, но, глядя на RwLock::read(), мы можем увидеть сходство с RefCell::заимствовать():

pub fn read(&self) -> LockResult<RwLockReadGuard<T>> {
    unsafe {
        self.inner.lock.read();
        RwLockReadGuard::new(&*self.inner, &self.data)
    }
}

Немного распутав это, мы видим, что сначала блокируется внутренняя блокировка3, затем создается ReadLockReadGuard. Аналогично тому, что BorrowRef делает для RefCell, RwLockReadGuard::new() преобразует необработанный указатель, полученный после вызова UnsafeCell::get(), в ссылку:

unsafe fn new(lock: &'rwlock StaticRwLock, data: &'rwlock UnsafeCell<T>)
              -> LockResult<RwLockReadGuard<'rwlock, T>> {
    poison::map_result(lock.poison.borrow(), |_| {
        RwLockReadGuard {
            __lock: lock,
            __data: &*data.get(),
        }
    })
}

И RwLock::write(), и Mutex::lock() следуют шаблону, аналогичному RwLock::read(), поэтому нет необходимости изучать их по отдельности.

Подведение итогов

В этой статье мы узнали, что внутренняя изменчивость не полагается на тайную магию, а достигается благодаря UnsafeCell, небольшой аккуратной структуре, которая позволяет создавать более эргономичные абстракции поверх нее.

Поскольку его API включает в себя небезопасные операции, его использование напрямую немного обременительно. Почти в каждом случае, когда нам нужна внутренняя изменчивость, нам лучше подходят Cell, RefCell, RwLock и Mutex. Есть несколько случаев, когда вы можете захотеть избежать накладных расходов этих типов напрямую со стороны UnsafeCell, например, реализовать новые блокировки и параллельные структуры данных, но это не обычные задачи, если вы не работаете в академических кругах. 😉

Ух! На этом мы завершаем серию статей о внутренней изменчивости. Я надеюсь, что вам понравилось! Особая благодарность всем, кто комментировал и эту, и предыдущие статьи. Мы глубоко признательны за вашу поддержку и исправления. 💜

Что мы должны исследовать дальше? Сообщите мне в Twitter (@meqif) или отправьте мне электронное письмо (words@ricardomartins.cc). Вы также можете обсудить статью на Reddit. Если вы не хотите пропустить следующие статьи, подпишитесь на мою рассылку, заполнив форму ниже. 👇

  1. Многие другие типы зависят от UnsafeCell, такие как Condvar, используемые структуры Sender и Receiver, созданные с помощью std::sync::mpsc::channel, локальные переменные потока (созданные с помощью макроса thread_local), частные структуры данных, используемые в реализация компилятора и некоторые другие, которые я не буду перечислять.

  2. Сравните результат при запуске этого примера в режиме отладки и в режиме выпуска. В режиме выпуска компилятор предполагает, что значение никогда не изменяется, поэтому вызов println! не показывает обновление! Спасибо /u/derKha и /u/notriddle за их примеры.

  3. Эта внутренняя блокировка фактически является встроенной блокировкой ОС. Вы можете прочитать реализации для Unix и Windows. Оба полагаются на UnsafeCell.