Как идиоматически использовать глобальные переменные в Rust?

Как идиоматически использовать глобальные переменные в Rust Изучение

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

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

Обзор

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

Есть много вариантов реализации глобального состояния в Rust

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

  • Без глобальных переменных: рефакторинг в Arc / Rc
  • Глобальные переменные, инициализированные во время компиляции: 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..1 (/playground)
error: free static item without body
 --> src/main.rs:21:1
  |
3 | static START_TIME;
  | ^^^^^^^^^^^^^^^^^-
  |                  |
  |                  help: provide a definition for the static: `= <expr>;`

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

Если вы переходите на 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<String> = 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{…}блок, и это сработало бы. Иногда это действенная стратегия. Возможно, чтобы проверить, работает ли остальная часть кода, как ожидалось. Но это не то идиоматическое решение, которое я хочу вам показать. Итак, давайте исследуем решения, безопасность которых компилятор гарантирует.

Читайте также:  Учебное пособие по React: создание приложения-калькулятора с нуля

Реорганизуйте пример

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

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

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.

Читайте также:  Прокси-сервер или VPN: 5 основных отличий, которые вы должны знать

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

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

use chrono::Utc;

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

fn main() {
    GLOBAL_DATA.with(|text| {
        println!("{}", *text);
    });
}

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

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

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

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

thread_local!(static GLOBAL_DATA: RefCell<String> = 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 «.)

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

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

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

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

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

static mut STD_ONCE_COUNTER: Option<Mutex<String>> = None;
static INIT: Once = Once::new();

fn global_string<'a>() -> &'a Mutex<String> {
    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<String> = 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 имеет тенденцию предоставлять пользователю максимальные возможности, что как побочный эффект усложняет задачу.

Оцените статью
bestprogrammer.ru
Добавить комментарий