本文首发公众号 猩猩程序员 欢迎关注
什么是 static mut?
在 Rust 中,static mut 用于定义可变的全局变量。与常规的静态变量不同,static mut 允许你在程序的不同部分修改该变量。然而,static mut 引入了“可变性与别名”的问题,特别是当你创建引用时,可能会导致 数据竞争 和 未定义行为。
2024 版本的改变
从 Rust 2024 版本开始,static_mut_refs 这个 lint 的默认级别变成了 deny。这意味着,如果你试图获取 static mut 的引用,编译器会报错。这一改动的目的是为了避免在程序中出现不安全的操作,从而保证更高的安全性。
为什么不推荐引用 static mut?
引用 static mut 在 Rust 中是危险的,因为它违反了 Rust 的“可变性和别名”规则(mutability XOR aliasing)。这个规则要求:
- 如果一个变量是可变的,那么它不能有多个引用(即不能同时有可变引用和不可变引用)。
- 如果一个变量有多个引用,则它必须是不可变的。
对于 static mut,这意味着你不能在程序中同时拥有多个对它的引用。如果你试图在多个地方修改它,Rust 的借用检查器将无法在编译时保证安全,可能导致数据竞争。
以前的版本
在 Rust 的早期版本中,虽然对 static mut 的引用会被标记为潜在的未定义行为,但没有强制禁止这种操作。程序员通常需要小心地管理 static mut,并自己避免同时出现多个引用。
例如,在 Rust 1.55 版本中,如果你尝试如下操作,编译器可能不会直接报错,而是发出警告,但程序依然会出现潜在的未定义行为:
static mut X: i32 = 23;
static mut Y: i32 = 24;
unsafe {
let x_ref = &X; // 警告:创建了对 `static mut` 的引用
let y_ref = &Y; // 警告:创建了对 `static mut` 的引用
}
2024 版本后的示例
现在在 Rust 2024 版本中,任何对 static mut 的引用都会直接导致编译错误:
static mut X: i32 = 23;
static mut Y: i32 = 24;
unsafe {
let x_ref = &X; // ERROR: 创建了对 `static mut` 的引用
let y_ref = &Y; // ERROR: 创建了对 `static mut` 的引用
}
即使你没有直接修改 static mut 的值,仅仅获取对它的引用也会导致错误。
自动隐式引用
有些情况下,Rust 会自动创建引用,而你并没有显式地使用 & 操作符。比如,下面的例子也会触发 static_mut_refs lint:
static mut NUMS: &[u8; 3] = &[0, 1, 2];
unsafe {
println!("{NUMS:?}"); // ERROR: 对 `static mut` 的隐式引用
let n = NUMS.len(); // ERROR: 对 `static mut` 的隐式引用
}
推荐方案
使用不可变静态变量
如果可能的话,最好使用不可变的静态变量,并通过某些抽象来实现内部可变性。例如,你可以使用 Cell 或 RefCell 来管理可变性,同时保证线程安全。
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn main() {
COUNTER.fetch_add(1, Ordering::Relaxed);
}
使用 Mutex 或 RwLock
如果你的类型比原子类型复杂,考虑使用 Mutex 或 RwLock 来确保全局值的正确访问:
static QUEUE: Mutex<VecDeque<String>> = Mutex::new(VecDeque::new());
fn main() {
QUEUE.lock().unwrap().push_back(String::from("abc"));
let first = QUEUE.lock().unwrap().pop_front();
}
使用 OnceLock 或 LazyLock
如果你需要一次性初始化一个静态变量,可以使用 OnceLock 或 LazyLock 来代替 static mut:
static STATE: LazyLock<GlobalState> = LazyLock::new(|| {
GlobalState::new()
});
fn main() {
STATE.example();
}
或者,如果需要传递初始化参数,可以使用 OnceLock:
static STATE: OnceLock<GlobalState> = OnceLock::new();
fn main() {
let args = parse_arguments();
let state = GlobalState::new(args.verbose);
let _ = STATE.set(state);
STATE.get().unwrap().example();
}
使用原始指针
如果你确实需要使用 static mut,可以通过原始指针来避免直接创建引用。例如,在调用 C 库时,可以使用原始指针:
static mut STATE: GlobalState = GlobalState::new();
unsafe extern "C" {
fn example_ffi(state: *mut GlobalState);
}
fn main() {
unsafe {
example_ffi(&raw mut STATE);
}
}
使用 Sync 和 UnsafeCell
如果你需要在 static mut 中实现内部可变性,可以通过实现 Sync 来解决。下面的代码示例展示了如何将 UnsafeCell 包装成 Sync 类型:
#[repr(transparent)]
pub struct SyncUnsafeCell<T>(UnsafeCell<T>);
unsafe impl<T: Sync> Sync for SyncUnsafeCell<T> {}
static STATE: SyncUnsafeCell<GlobalState> = SyncUnsafeCell(UnsafeCell::new(GlobalState::new()));
fn set_value(value: i32) {
with_interrupts_disabled(|| {
let state = STATE.0.get();
unsafe {
(*state).value = value;
}
});
}
最好的做法是尽量避免使用可变的全局变量。虽然有时这可能会让代码更麻烦,尤其是在需要传递可变引用时,但通过合适的设计,可以避免这些不安全的操作。
本文首发公众号 猩猩程序员 欢迎关注