×

如何在Rust中合理使用全局变量

作者:Terry2021.08.04来源:Web前端之家浏览:19960评论:0
关键词:jsRust

500.jpg

在 Rust 中声明和使用全局变量可能很棘手。通常对于这种语言,Rust 通过强制我们非常明确来确保健壮性。

在本文中,我将讨论 Rust 编译器想要让我们避免的陷阱。然后我将向您展示适用于不同场景的最佳解决方案。

概述

在 Rust 中实现全局状态有很多选择。如果您赶时间,这里是我的建议的快速概述。

QQ截图20210804090258.jpg

您可以通过以下链接跳转到本文的特定部分:

  • 没有全局变量:重构为 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关键字不能在全球范围内使用。我们只能使用staticor 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: `= <expr>;`

所以这也行不通!在任何用户代码运行之前,所有静态值都必须完全初始化且有效。

如果您从另一种语言(例如 JavaScript 或 Python)转向 Rust,这可能看起来是不必要的限制。但是任何 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{...}块中,它会起作用。有时,这是一种有效的策略。也许是为了测试代码的其余部分是否按预期工作。但这不是我想向您展示的惯用解决方案。因此,让我们探索编译器保证安全的解决方案。

重构示例

您可能已经注意到这个例子根本不需要全局变量。通常情况下,如果我们能想到一个没有全局变量的解决方案,我们应该避免它们。

这里的想法是将声明放在 main 函数中:

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`

这个错误并不明显。编译器告诉我们,产生的线程可能比 value 存活的时间长start_time,它位于 main 函数的堆栈帧中。

从技术上讲,我们可以看到这是不可能的。线程被加入,因此主线程不会在子线程完成之前退出。

但是编译器不够聪明,无法找出这种特殊情况。通常,当产生一个新线程时,提供的闭包只能借用具有静态生命周期的项目。换句话说,借用的值必须在整个程序生命周期内都有效。

对于任何刚刚学习 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<String>>将线程安全的内部可变性添加到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)。

但是,如果您需要在运行时计算值,或者需要堆分配,则conststatic没有帮助。

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的锁定开销。

线程局部变量的绝对性能高度依赖于平台。但是我在自己的 PC 上做了一些快速测试,将它与依赖锁的内部可变性进行比较,发现它快了 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 以在浏览器中运行时也适用。在这种情况下,使用对多线程代码安全的方法将是矫枉过正。

关于线程局部变量的一个警告是它们的实现取决于平台。通常,您不会注意到这一点,但请注意,因此删除语义是平台相关的。

尽管如此,多线程全局变量的解决方案显然也适用于单线程情况。没有内部可变性,这些似乎和线程局部变量一样快。

那么接下来让我们来看看。

带运行时初始化的多线程全局变量

对于具有运行时初始化的安全全局变量,标准库目前没有很好的解决方案。但是,使用std::sync::Once unsafe,如果您知道自己在做什么,则可以构建安全使用的东西。

官方文档中的示例[https://doc.rust-lang.org/std/sync/struct.Once.html#examples-1]是一个很好的起点。如果您还需要内部可变性,则必须将该方法与读写锁或互斥锁相结合。这可能是这样的:

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 年。

一旦 Cell目前被考虑用于标准库。(请参阅此跟踪问题[https://github.com/rust-lang/rust/issues/74465]。)如果您使用的是夜间编译器,您已经可以通过添加#![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,它是目前最流行的用于初始化全局变量的 crate。它使用带有小语法扩展 ( static ref)的宏来定义全局变量。

这里又是同一个例子,从once_cellto翻译过来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在 aMutex或 中RwLock

结论

这些都是我所知道的在 Rust 中实现全局变量的所有(明智的)方法。我希望它更简单。但是全局状态本质上是复杂的。结合 Rust 的内存安全保证,一个简单的包罗万象的解决方案似乎是不可能的。但我希望这篇文章能帮助您了解过多的可用选项。

一般来说,Rust 社区倾向于为用户提供最大的权力——这使得事情变得更加复杂。

您的支持是我们创作的动力!
温馨提示:本文作者系Terry ,经Web前端之家编辑修改或补充,转载请注明出处和本文链接:
https://jiangweishan.com/article/js20210804a3.html

网友评论文明上网理性发言 已有0人参与

发表评论: