在 Rust 中声明和使用全局变量可能很棘手。通常对于这种语言,Rust 通过强制我们非常明确来确保健壮性。
在本文中,我将讨论 Rust 编译器想要让我们避免的陷阱。然后我将向您展示适用于不同场景的最佳解决方案。
概述
在 Rust 中实现全局状态有很多选择。如果您赶时间,这里是我的建议的快速概述。
您可以通过以下链接跳转到本文的特定部分:
-
编译时初始化的全局变量: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
or 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
)。
但是,如果您需要在运行时计算值,或者需要堆分配,则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的锁定开销。
线程局部变量的绝对性能高度依赖于平台。但是我在自己的 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_cell
to翻译过来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 社区倾向于为用户提供最大的权力------这使得事情变得更加复杂。