Разработка сайтов в Краснодоне, ЛНР. Как идиоматически использовать глобальные переменные в Rust

Объявление и использование глобальных переменных в Rust может быть сложным. Как правило, для этого языка Rust обеспечивает надежность, заставляя нас быть очень явными.

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

Обзор

Есть много вариантов реализации глобального состояния в Rust. Если вы спешите, вот краткий обзор моих рекомендаций.

Блок-схема для поиска наилучшего решения для глобальных переменных

Вы можете перейти к определенным разделам этой статьи по следующим ссылкам:

Неглобальный: рефакторинг в Arc/ Рк

Глобальные переменные, инициализируемые во время компиляции: const T / static T

Используйте внешнюю библиотеку для простой инициализации глобальных переменных во время выполнения: lazy_static/once_cell

Реализуйте свою собственную инициализацию во время выполнения: std: sync: Once + static mut T

Специализированный случай для однопоточной инициализации среды выполнения: thread_local

Наивная первая попытка использования глобальных переменных в Rust

Давайте начнем с примера того, как не следует использовать глобальные переменные. Предположим, я хочу сохранить время запуска программы в глобальной строке. Позже я хочу получить доступ к значению из нескольких потоков.

У новичка в Rust может возникнуть соблазн объявить глобальную переменную точно так же, как любую другую переменную в Rust, используя let. Тогда полная программа может выглядеть так:

use chrono: Utc;

let START_TIME = Utc: now ().to_string () ;

pub fn main () {

let thread_1 = std: thread: spawn (||{

println! («Started {}, called thread 1 {}», START_TIME.as_ref ().unwrap (), Utc: now ());

}) ;

let thread_2 = std: thread: spawn (||{

println! («Started {}, called thread 2 {}», START_TIME.as_ref ().unwrap (), Utc: now ());

}) ;

// Join threads and panic on error to show what went wrong

thread_1.join ().unwrap () ;

thread_2.join ().unwrap () ;

}

Попробуйте сами на детской площадке!

Это недопустимый синтаксис для Rust. Ключевое letслово нельзя использовать в глобальной области. Мы можем использовать только staticили const. Последний объявляет истинную константу, а не переменную. Только staticдает нам глобальную переменную.

Причина этого заключается в том, что letпеременная выделяется в стеке во время выполнения. Обратите внимание, что это остается верным при размещении в куче, как в let t = Box: new () ;. В сгенерированном машинном коде все еще есть указатель на кучу, который сохраняется в стеке.

Глобальные переменные хранятся в сегменте данных программы. У них есть фиксированный адрес, который не меняется во время выполнения. Следовательно, сегмент кода может включать постоянные адреса и вообще не требует места в стеке.

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

Попробуем еще раз с static:

use chrono: Utc;

static START_TIME: String = Utc: now ().to_string () ;

pub fn main () {

//...

}

Компилятор пока недоволен:

error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants

—> src/main.rs:3:24

|

3 | static start: String = Utc: now ().to_string () ;

| ^^^^^^^^^^^^^^^^^^^^^^

Хм, так что значение инициализации статической переменной не может быть вычислено во время выполнения. Тогда, может быть, просто позволить ему быть неинициализированным?

use chrono: Utc;

static START_TIME;

pub fn main () {

//...

}

Это дает новую ошибку:

Compiling playground v0.0.1 (/playground)

error: free static item without body

—> src/main.rs:21:1

|

3 | static START_TIME;

| ^^^^^^^^^^^^^^^^^-

| |

| help: provide a definition for the static: `= ; `

Так тоже не работает! Все статические значения должны быть полностью инициализированы и действительны до запуска любого пользовательского кода.

Если вы переходите на Rust с другого языка, такого как JavaScript или Python, это может показаться ненужным ограничением. Но любой гуру C++ может рассказать вам истории о фиаско статического порядка инициализации, который может привести к неопределенному порядку инициализации, если мы не будем осторожны.

Например, представьте что-то вроде этого:

static A: u32 = foo () ;

static B: u32 = foo () ;

static C: u32 = A + B;

fn foo () → u32 {

C + 1

}

fn main () {

println! («A: {} B: {} C: {}», A, B, C) ;

}

В этом фрагменте кода нет безопасного порядка инициализации из-за циклических зависимостей.

Если бы это был C++, который не заботится о безопасности, результат был бы A: 1 B: 1 C: 2. Он инициализируется нулями перед запуском любого кода, а затем порядок определяется сверху вниз в каждой единице компиляции.

По крайней мере, определено, каков результат. Однако «фиаско» начинается, когда статические переменные из разных.cppфайлов, а значит, из разных единиц компиляции. Тогда порядок не определен и обычно зависит от порядка файлов в командной строке компиляции.

В Rust нулевая инициализация не имеет значения. В конце концов, ноль является недопустимым значением для многих типов, таких как Box. Кроме того, в Rust мы не принимаем странные проблемы с порядком. Пока мы держимся подальше от unsafe, компилятор должен позволять нам писать только разумный код. И именно поэтому компилятор не позволяет нам использовать прямую инициализацию во время выполнения.

Но можно ли обойти инициализацию, используя Noneэквивалент нулевого указателя? По крайней мере, это все в соответствии с системой типов Rust. Конечно, я могу просто переместить инициализацию в начало основной функции, верно?

static mut START_TIME: Option = None;

pub fn main () {

START_TIME = Some (Utc: now ().to_string ());

//...

}

Ах, ну, ошибка, которую мы получаем, это...

error[E0133]: use of mutable static is unsafe and requires unsafe function or block

—> src/main.rs:24:5

|

6 | START_TIME = Some (Utc: now ().to_string ());

| ^^^^^^^^^^ use of mutable static

|

= note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior

В этот момент я мог бы обернуть его в unsafe{... }блок, и он бы работал. Иногда это верная стратегия. Возможно, чтобы проверить, работает ли остальная часть кода должным образом. Но это не идиоматическое решение, которое я хочу вам показать. Итак, давайте рассмотрим решения, безопасность которых гарантирована компилятором.

Рефакторинг примера

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

Идея здесь состоит в том, чтобы поместить объявление внутри основной функции:

pub fn main () {

let start_time = Utc: now ().to_string () ;

let thread_1 = std: thread: spawn (||{

println! («Started {}, called thread 1 {}», &start_time, Utc: now ());

}) ;

let thread_2 = std: thread: spawn (||{

println! («Started {}, called thread 2 {}», &start_time, Utc: now ());

}) ;

// Join threads and panic on error to show what went wrong

thread_1.join ().unwrap () ;

thread_2.join ().unwrap () ;

}

Единственная проблема — заимствованный чекер:

error[E0373]: closure may outlive the current function, but it borrows `start_time`, which is owned by the current function

—> src/main.rs:42:39

|

42 | let thread_1 = std: thread: spawn (||{

| ^^ may outlive borrowed value `start_time`

43 | println! («Started {}, called thread 1 {}», &start_time, Utc: now ());

| ---------- `start_time` is borrowed here

|

note: function requires argument type to outlive `'static`

Эта ошибка не совсем очевидна. Компилятор сообщает нам, что порожденный поток может жить дольше, чем значение start_time, которое находится в кадре стека основной функции.

Технически мы видим, что это невозможно. Потоки объединяются, поэтому основной поток не завершится до завершения дочерних потоков.

Но компилятор недостаточно умен, чтобы разобраться в этом конкретном случае. Как правило, при создании нового потока предоставленное замыкание может заимствовать элементы только со статическим временем жизни. Другими словами, заимствованные значения должны быть активны в течение всего времени существования программы.

Для тех, кто только знакомится с Rust, это может стать моментом, когда вы захотите обратиться к глобальным переменным. Но есть по крайней мере два решения, которые намного проще. Самый простой способ — клонировать строковое значение, а затем передать права собственности на строки замыканиям. Конечно, это требует дополнительного распределения и некоторой дополнительной памяти. Но в данном случае это всего лишь короткая строка и ничего критичного для производительности.

Но что, если бы это был гораздо более крупный объект, которым можно было бы поделиться? Если вы не хотите его клонировать, оберните его интеллектуальным указателем с подсчетом ссылок. Rc — это однопоточный тип с подсчетом ссылок. Arc — это атомарная версия, которая может безопасно обмениваться значениями между потоками.

Итак, чтобы удовлетворить компилятор, мы можем использовать Arcследующее:

/* Final Solution */

pub fn main () {

let start_time = Arc: new (Utc: now ().to_string ());

// This clones the Arc pointer, not the String behind it

let cloned_start_time = start_time.clone () ;

let thread_1 = std: thread: spawn (move ||{

println! («Started {}, called thread 1 {}», cloned_start_time, Utc: now ());

}) ;

let thread_2 = std: thread: spawn (move ||{

println! («Started {}, called thread 2 {}», start_time, Utc: now ());

}) ;

// Join threads and panic on error to show what went wrong

thread_1.join ().unwrap () ;

thread_2.join ().unwrap () ;

}

Попробуйте сами на детской площадке!

Это был краткий обзор того, как разделить состояние между потоками, избегая глобальных переменных. Помимо того, что я показал вам до сих пор, вам также может понадобиться внутренняя изменчивость для изменения общего состояния. Полное описание внутренней изменчивости выходит за рамки этой статьи. Но в этом конкретном примере я бы предпочел Arc<Mutex>добавить в start_time.

Когда значение глобальной переменной известно во время компиляции

По моему опыту, наиболее распространенными вариантами использования глобального состояния являются не переменные, а константы. В Rust они бывают двух видов:

Постоянные значения, определенные с помощью const. Они встроены компилятором. Внутренняя изменчивость никогда не допускается.

Статические переменные, определенные с помощью static. Они получают фиксированное место в сегменте данных. Возможна внутренняя изменчивость.

Оба они могут быть инициализированы константами времени компиляции. Это могут быть простые значения, такие как 42или «hello world». Или это может быть выражение, включающее несколько других констант времени компиляции и функций, помеченных как const. Пока мы избегаем циклических зависимостей. (Вы можете найти более подробную информацию о константных выражениях в The Rust Reference.)

use std: sync: atomic: AtomicU64;

use std: sync: {Arc, Mutex};

static COUNTER: AtomicU64 = AtomicU64: new (TI_BYTE) ;

const GI_BYTE: u64 = 1024 * 1024 * 1024;

const TI_BYTE: u64 = 1024 * GI_BYTE;

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

Если вам нужна внутренняя изменчивость, есть несколько вариантов. Для большинства примитивов есть соответствующий атомарный вариант, доступный в std: sync: atomic. Они предоставляют чистый API для атомарной загрузки, хранения и обновления значений.

При отсутствии атомарности обычно выбирают замок. Стандартная библиотека Rust предлагает блокировку чтения-записи (RwLock) и блокировку взаимного исключения (Mutex).

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

Однопоточные глобальные переменные в Rust с инициализацией во время выполнения

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

Однако мы не должны использовать static mutнапрямую и обертывать доступ unsafeтолько потому, что есть только один поток. Таким образом, мы можем столкнуться с серьезным повреждением памяти.

Например, небезопасное заимствование из глобальной переменной может дать нам несколько изменяемых ссылок одновременно. Затем мы могли бы использовать один из них для перебора вектора, а другой — для удаления значений из того же вектора. Затем итератор мог выйти за допустимую границу памяти, потенциальный сбой, который предотвратил бы безопасный Rust.

Но в стандартной библиотеке есть способ «глобально» хранить значения для безопасного доступа в рамках одного потока. Я говорю о местных жителях треда. При наличии множества потоков каждый поток получает независимую копию переменной. Но в нашем случае с одним потоком есть только одна копия.

Локальные переменные потока создаются с помощью thread_local! макроса. Доступ к ним требует использования замыкания, как показано в следующем примере:

use chrono: Utc;

thread_local! (static GLOBAL_DATA: String = Utc: now ().to_string ());

fn main () {

GLOBAL_DATA.with (|text| {

println! («{}», *text) ;

}) ;

}

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

Локальные потоки действительно хороши, когда дело доходит до внутренней изменчивости. В отличие от всех других решений, для него не требуется Sync. Это позволяет использовать RefCell для внутренней изменчивости, что позволяет избежать накладных расходов на блокировку Mutex.

Абсолютная производительность локальных потоков сильно зависит от платформы. Но я провел несколько быстрых тестов на своем собственном ПК, сравнив его с внутренней изменчивостью, основанной на блокировках, и обнаружил, что он работает в 10 раз быстрее. Я не ожидаю, что результат будет перевернут на какой-либо платформе, но обязательно запустите свои собственные тесты, если вам очень важна производительность.

Вот пример того, как использовать RefCellвнутреннюю изменчивость:

thread_local! (static GLOBAL_DATA: RefCell = RefCell: new (Utc: now ().to_string ()));

fn main () {

GLOBAL_DATA.with (|text| {

println! («Global string is {}», *text.borrow ());

}) ;

GLOBAL_DATA.with (|text| {

*text.borrow_mut () = Utc: now ().to_string () ;

}) ;

GLOBAL_DATA.with (|text| {

println! («Global string is {}», *text.borrow ());

}) ;

}

Попробуйте сами на детской площадке!

В качестве примечания, хотя потоки в WebAssembly отличаются от потоков на платформе x86_64, этот шаблон с thread_local! + RefCellтакже применим при компиляции Rust для запуска в браузере. Использование подхода, безопасного для многопоточного кода, в этом случае было бы излишним. (Если идея запуска Rust внутри браузера для вас нова, не стесняйтесь прочитать предыдущую статью, которую я написал, под названием «Учебник по Rust: введение в Rust для разработчиков JavaScript «.)

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

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

Итак, давайте посмотрим на это дальше.

Многопоточные глобальные переменные с инициализацией во время выполнения

В стандартной библиотеке в настоящее время нет отличного решения для безопасных глобальных переменных с инициализацией во время выполнения. Однако, используя std: sync: Once, можно создать что-то unsafeбезопасное, если вы знаете, что делаете.

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

static mut STD_ONCE_COUNTER: Option<Mutex> = None;

static INIT: Once = Once: new () ;

fn global_string<'a> () → &'a Mutex {

INIT.call_once (|| {

// Since this access is inside a call_once, before any other accesses, it is safe

unsafe {

*STD_ONCE_COUNTER.borrow_mut () = Some (Mutex: new (Utc: now ().to_string ()));

}

}) ;

// As long as this function is the only place with access to the static variable,

// giving out a read-only borrow here is safe because it is guaranteed no more mutable

// references will exist at this point or in the future.

unsafe { STD_ONCE_COUNTER.as_ref ().unwrap () }

}

pub fn main () {

println! («Global string is {}», *global_string ().lock ().unwrap ());

*global_string ().lock ().unwrap () = Utc: now ().to_string () ;

println! («Global string is {}», *global_string ().lock ().unwrap ());

}

Попробуйте сами на детской площадке!

Если вы ищете что-то более простое, я настоятельно рекомендую один из двух ящиков, о которых я расскажу в следующем разделе.

Внешние библиотеки для управления глобальными переменными в Rust

Основываясь на популярности и личном вкусе, я хочу порекомендовать две библиотеки, которые, по моему мнению, являются лучшим выбором для простых глобальных переменных в Rust по состоянию на 2021 год.

Once Cell в настоящее время рассматривается как стандартная библиотека. (См. эту проблему с отслеживанием.) Если вы используете ночной компилятор, вы уже можете использовать для него нестабильный API, добавив #![feature (once_cell) ]в файл main.rs.

Вот пример использования once_cellстабильного компилятора с дополнительной зависимостью:

use once_cell: sync: Lazy;

static GLOBAL_DATA: Lazy = Lazy: new (||Utc: now ().to_string ());

fn main () {

println! («{}», *GLOBAL_DATA) ;

}

Попробуйте сами на детской площадке!

Наконец, есть также Lazy Static, в настоящее время самый популярный крейт для инициализации глобальных переменных. Он использует макрос с небольшим расширением синтаксиса (static ref) для определения глобальных переменных.

Вот тот же пример снова, переведенный с once_cellна lazy_static:

#[macro_use]

extern crate lazy_static;

lazy_static! (

static ref GLOBAL_DATA: String = Utc: now ().to_string () ;

) ;

fn main () {

println! («{}», *GLOBAL_DATA) ;

}

Попробуйте сами на детской площадке!

Выбор между once_cellи lazy_staticпо существу сводится к тому, какой синтаксис вам больше нравится.

Кроме того, оба поддерживают внутреннюю изменчивость. Просто оберните Stringв Mutexили RwLock.

Заключение

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

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

Может быть сложно уследить за всеми деталями. В результате я провожу много свободного времени, играя с функциями Rust, чтобы исследовать возможности. В процессе я обычно реализую более мелкие или более крупные хобби-проекты — например, видеоигры — и загружаю их в свой профиль на GitHub. Затем, если я нахожу что-то интересное в своих экспериментах с языком, я пишу об этом в свой приватный блог. Проверьте это, если вы хотите прочитать больше подробностей о Rust!

Делитесь нашими материалами с друзьями!

 

 

Заказать разработку сайта