Типы обертки в Rust: выбор гарантий
Перевод | Автор оригинала: Manish Goregaokar
Этот пост стал частью официальной книги Rust
В моем предыдущем посте я немного рассказал о том, почему шаблон RWlock важен для доступа к данным, поэтому Rust применяет этот шаблон либо во время компиляции, либо во время выполнения, в зависимости от используемых абстракций.
Мне пришло в голову, что в Rust существует множество таких абстракций, каждая со своими уникальными гарантиями. У программиста снова есть выбор между принудительным исполнением и временем компиляции. Мне пришло в голову, что это множество «типов обертки» 1 может устрашить новичков; В этом посте я намерен дать подробное объяснение того, что делают некоторые известные из них и когда их следует использовать.
Я предполагаю, что читатель знает о владении и заимствовании в Rust. Тем не менее, я постараюсь, чтобы большая часть этого поста была доступна для тех, кто еще не знаком с этими концепциями. Помимо двух ссылок на книгу выше, эти два сообщения в блоге подробно освещают эту тему.
Основные типы указателей
Коробка
Box
let x = Box::new(1);
let y = x;
// x no longer accessible here
Здесь коробка была перемещена в y. Поскольку x больше не владеет им, компилятор больше не позволит программисту использовать x после этого. Блок аналогичным образом может быть перемещен из функции путем возврата, и когда блок (тот, который не был перемещен) выходит за пределы области видимости, запускаются деструкторы, освобождая внутренние данные.
Эта абстракция представляет собой недорогую абстракцию для динамического распределения. Если вы хотите выделить часть памяти в куче и безопасно передать указатель на эту память, это идеально. Обратите внимание, что вам будет разрешено делиться заимствованными ссылками на это только с помощью обычных правил заимствования, проверенных во время компиляции.
Интерлюдия: Копировать
Семантика перемещения / владения не является особенной для Box
Тип копирования - это тип, в котором все данные, которые он логически охватывает (обычно владеет), являются частью его представления в стеке2. Большинство типов, содержащих указатели на другие данные, не являются копией, поскольку в другом месте есть дополнительные данные, и простое копирование представления стека может случайно разделить владение этими данными небезопасным образом.
Такие типы, как Vec
&T и необработанные указатели - это Копировать. Несмотря на то, что они указывают на дополнительные данные, они не «владеют» этими данными. В то время как Box
Фактически, тип может быть копируемым, если копия его представления в стеке не нарушает безопасность памяти.
&T и &mut T
Это неизменяемые и изменяемые ссылки соответственно. Они следуют шаблону «блокировка чтения-записи», описанному в моем предыдущем посте, так что можно иметь либо только одну изменяемую ссылку на некоторые данные, либо любое количество неизменяемых, но не то и другое одновременно. Эта гарантия применяется во время компиляции и не требует видимых затрат во время выполнения. В большинстве случаев таких указателей достаточно для обмена дешевыми ссылками между разделами кода.
Эти указатели не могут быть скопированы таким образом, чтобы они пережили связанный с ними срок службы.
* const T и * mut T
Это необработанные указатели в стиле C без привязки к времени жизни или праву собственности. Они просто указывают на какое-то место в памяти без каких-либо других ограничений. Единственная гарантия, которую они предоставляют, - то, что они не могут быть разыменованы, за исключением кода, помеченного как небезопасный.
Они полезны при создании безопасных и недорогих абстракций, таких как Vec
Rc
Это первая оболочка, которую мы рассмотрим, которая требует затрат времени выполнения.
Rc
Внутри он содержит общий «счетчик ссылок», который увеличивается каждый раз, когда клонируется Rc, и уменьшается каждый раз, когда один из Rcs выходит за пределы области видимости. Основная ответственность Rc
Внутренние данные здесь неизменяемы, и если создается цикл ссылок, данные будут утекать. Если нам нужны данные, которые не утекают во время циклов, нам нужен сборщик мусора. Я не знаю ни одного существующего сборщика мусора в Rust, но я работаю над одним с Никой Лейзелл, а есть еще один цикл сбора одного, написанный Ником Фицджеральдом.
Гарантии
Основная гарантия, представленная здесь, заключается в том, что данные не будут уничтожены до тех пор, пока все ссылки на них не выйдут за рамки.
Это следует использовать, когда вы хотите динамически распределять и совместно использовать некоторые данные (только для чтения) между различными частями вашей программы, когда неизвестно, какая часть завершит использование указателя последней. Это жизнеспособная альтернатива &T, когда &T либо невозможно статически проверить на правильность, либо создает крайне неэргономичный код, с которым программист не желает тратить затраты на разработку, работая с.
Этот указатель не является потокобезопасным, и Rust не позволяет отправлять его или передавать другим потокам. Это позволяет избежать затрат на атомики в ситуациях, когда они не нужны.
У этого есть родственный умный указатель, Weak
Расходы
Что касается памяти, Rc
Rc
Типы ячеек
«Клетки» обеспечивают внутреннюю изменчивость. Другими словами, они содержат данные, которыми можно манипулировать, даже если тип не может быть получен в изменяемой форме (например, когда он стоит за & -ptr или Rc
В документации на модуль ячейки есть довольно хорошее объяснение этого.
Эти типы обычно находятся в полях структуры, но их можно найти и в других местах.
Ячейка
Cell
С помощью этой оболочки все еще можно нарушить собственные инварианты, поэтому будьте осторожны при ее использовании. Если поле заключено в ячейку, это хороший показатель того, что фрагмент данных является изменяемым и может не оставаться неизменным между моментом, когда вы его впервые читаете, и тем, когда вы собираетесь его использовать.
let x = Cell::new(1);
let y = &x;
let z = &x;
x.set(2);
y.set(3);
z.set(4);
println!("{}", x.get());
Обратите внимание, что здесь мы смогли изменить одно и то же значение из различных неизменяемых ссылок.
Это имеет те же затраты времени выполнения, что и следующее:
let mut x = 1;
let y = &mut x;
let z = &mut x;
x = 2;
*y = 3;
*z = 4;
println!("{}", x;
но у него есть дополнительное преимущество - фактическая успешная компиляция.
Гарантии
Это ослабляет ограничение «отсутствие псевдонимов с изменяемостью» там, где в этом нет необходимости. Однако это также ослабляет гарантии, предоставляемые ограничением; поэтому, если инварианты зависят от данных, хранящихся в Cell, следует быть осторожным.
Это полезно для изменения примитивов и других типов копирования, когда нет простого способа сделать это в соответствии со статическими правилами & и &mut.
Габор Лехель довольно кратко резюмировал гарантии, предоставленные Cell:
Основная гарантия, которую мы должны гарантировать, - это то, что внутренние ссылки не могут быть признаны недействительными (оставлены висящими) в результате мутации внешней структуры. (Подумайте о ссылках на внутренности таких типов, как Option, Box, Vec и т.д.) &, &Mut и Cell делают здесь разные компромиссы. & разрешает общие внутренние ссылки, но запрещает мутации; &mut разрешает мутацию xor внутренних ссылок, но не разрешает совместное использование; Ячейка допускает общую изменчивость, но не внутренние ссылки.
В конечном итоге, хотя общая изменчивость может вызвать множество логических ошибок (как описано в моем предыдущем посте ), это может вызвать ошибки безопасности памяти только в сочетании с «внутренними ссылками». Это для тех, у кого есть «интерьер», тип / размер которого можно изменять. Одним из примеров этого является перечисление Rust; где, изменяя вариант, вы можете изменить, какой тип содержится. Если у вас есть псевдоним внутреннего типа во время изменения варианта, указатели внутри этого псевдонима могут быть недействительными. Точно так же, если вы измените длину вектора, когда у вас есть псевдоним для одного из его элементов, этот псевдоним может стать недействительным.
Поскольку Cell не позволяет ссылаться на внутреннюю часть типа (вы можете только копировать и копировать обратно), перечисления и структуры могут быть одинаково изменяемыми в пределах этого псевдонима.
Этот комментарий Эдди также касается гарантий Cell и альтернатив.
Расходы
Использование Cell
RefCell
RefCell
Вместо этого у него есть затраты времени выполнения. RefCell
let x = RefCell::new(vec![1,2,3,4]);
{
println!("{:?}", *x.borrow())
}
{
let my_ref = x.borrow_mut();
my_ref.push(1);
}
Подобно Cell, это в основном полезно в ситуациях, когда сложно или невозможно удовлетворить контролера заемных средств. Обычно известно, что такие мутации не происходят во вложенной форме, но это хорошо.
Для больших и сложных программ становится полезным помещать некоторые вещи в RefCells, чтобы упростить работу. Например, многие карты в структуре ctxt во внутреннем устройстве компилятора Rust находятся внутри этой оболочки. Они изменяются только один раз (во время создания, а не сразу после инициализации) или пару раз в хорошо разделенных местах. Однако, поскольку эта структура повсеместно используется повсеместно, манипулирование изменяемыми и неизменяемыми указателями будет затруднено (возможно, невозможно) и, вероятно, образует суп из & -ptrs, который будет трудно расширить. С другой стороны, RefCell предоставляет дешевый (не нулевой) способ безопасного доступа к ним. В будущем, если кто-то добавит код, который пытается изменить ячейку, когда она уже заимствована, это вызовет (обычно детерминированную) панику, которая может быть связана с ошибочным заимствованием.
Точно так же в DOM Серво происходит множество мутаций, большая часть которых локальна для типа DOM, но некоторые из них пересекают DOM и изменяют различные вещи. Использование RefCell и Cell для защиты всех мутаций позволяет нам не беспокоиться об изменчивости повсюду, и одновременно выделяет места, где мутации действительно происходят.
Обратите внимание, что RefCell следует избегать, если с помощью указателей & возможно наиболее простое решение.
Гарантии
RefCell ослабляет статические ограничения, предотвращающие мутации с псевдонимом, и заменяет их динамическими. Таким образом, гарантии не изменились.
Расходы
RefCell не выделяет, но содержит дополнительный индикатор «состояния заимствования» (размером в одно слово) вместе с данными.
Во время выполнения каждое заимствование вызывает модификацию / проверку счетчика ссылок.
Синхронные типы
Многие из перечисленных выше типов нельзя использовать потокобезопасным образом. В частности, таким образом нельзя использовать Rc
Обратите внимание, что типы, не связанные с потоками, не могут быть отправлены между потоками, и это проверяется во время компиляции. Я расскажу, как это делается, в одном из следующих сообщений блога.
В модуле синхронизации есть много полезных оболочек для параллельного программирования, но я остановлюсь только на основных.
Дуга
Arc
Shared_ptr в C++ похож на Arc, однако в случае C++ внутренние данные всегда изменяемы. Для семантики, аналогичной семантике C++, мы должны использовать Arc<Mutex
Гарантии
Как и Rc, это обеспечивает (потокобезопасную) гарантию того, что деструктор для внутренних данных будет запущен, когда последняя дуга выйдет из области видимости (без каких-либо циклов).
Расходы
Это связано с дополнительными затратами на использование атомики для изменения счетчика ссылок (что будет происходить всякий раз, когда он клонируется или выходит за рамки). При совместном использовании данных из Arc в одном потоке, по возможности, предпочтительнее предоставлять общий доступ к указателям.
Mutex и RwLock
Mutex
{
let guard = mutex.lock();
// guard dereferences mutably to the inner type
*guard += 1;
} // lock released when destructor runs
RwLock имеет дополнительное преимущество в том, что он эффективен при многократном чтении. Всегда безопасно иметь несколько читателей для общих данных, пока нет писателей; а RwLock позволяет читателям получить «блокировку чтения». Такие блокировки могут быть получены одновременно и отслеживаются с помощью счетчика ссылок. Писатели должны получить «блокировку записи», которая может быть получена только тогда, когда все читатели выйдут за пределы области действия.
Гарантии
Оба они обеспечивают безопасную совместную изменчивость между потоками, однако они подвержены взаимоблокировкам. Некоторый уровень дополнительной безопасности протокола может быть получен с помощью системы типов. Примером этого является rust-sessions, экспериментальная библиотека, которая использует типы сессий для обеспечения безопасности протокола.
Расходы
Они используют внутренние атомарные типы для поддержки блокировок, и они аналогичны довольно дорого (они могут блокировать все операции чтения памяти через процессоры, пока они не будут выполнены). Ожидание этих блокировок также может быть медленным при большом количестве одновременного доступа.
Сочинение
Распространенная проблема при чтении кода на Rust - это такие вещи, как Rc <RefCell <Vec
Обычно это тот случай, когда нужно составить вместе необходимые гарантии, не платя за ненужные вещи.
Например, Rc <RefCell
Теперь мы можем пойти дальше и иметь Rc <RefCell <Vec
В первом случае RefCell оборачивает Vec, поэтому Vec полностью является изменяемым. В то же время может быть только одно изменяемое заимствование всего Vec в данный момент времени. Это означает, что ваш код не может одновременно работать с разными элементами вектора из разных дескрипторов Rc. Тем не менее, мы можем нажимать и выталкивать из Vec по желанию. Это похоже на &mut Vec
В последнем случае заимствованы отдельные элементы, но общий вектор неизменен. Таким образом, мы можем независимо заимствовать отдельные элементы, но не можем выталкивать или выталкивать из вектора. Это похоже на &mut [T] 5, но, опять же, проверка заимствования выполняется во время выполнения.
В параллельных программах мы имеем аналогичную ситуацию с Arc<Mutex
При чтении кода, который их использует, переходите к шагу за шагом и смотрите на предоставленные гарантии / затраты.
Выбирая составной тип, мы должны делать обратное; выяснить, какие гарантии мы хотим, и в какой точке композиции они нам нужны. Например, если есть выбор между Vec<RefCell
Обсудить: HN, Reddit
-
Я не уверен, что это технический термин для них, но я буду называть их так на протяжении всего поста.
-
Под «представлением стека» я подразумеваю данные в стеке, когда в стеке хранится значение этого типа. Например, Vec
имеет стековое представление указателя и двух целых чисел (длина, емкость). Хотя за косвенным указателем стоит больше данных, они не являются частью хранимой в стеке части Vec. Если посмотреть на это с другой стороны, типом будет Копировать, если копия данных копирует все данные, которыми она владеет. -
Arc<UnsafeCell
> на самом деле не будет компилироваться, поскольку UnsafeCell не является Send или Sync, но мы можем обернуть его в тип и реализовать Send / Sync для него вручную, чтобы получить Arc<Wrapper > где Wrapper - это struct Wrapper (UnsafeCell ). -
Под этим я подразумеваю часть данных, которая требует монотонной согласованности; т.е. счетчик или монотонно растущий стек
-
&[T] и &mut [T] - срезы; они состоят из указателя и длины и могут относиться к части вектора или массива. Элементы &mut [T] могут изменяться, однако его длина не может быть изменена.