如果是单线程的,可变静态原语实际上是“不安全的”吗?

Are mutable static primitives actually `unsafe` if single-threaded?

我正在开发单核嵌入式芯片。在 C 和 C++ 中,静态定义可全局使用的可变值是很常见的。 Rust 等价物大致是这样的:

static mut MY_VALUE: usize = 0;

pub fn set_value(val: usize) {
    unsafe { MY_VALUE = val }
}

pub fn get_value() -> usize {
    unsafe { MY_VALUE }
}

现在任何地方都可以调用免费函数 get_valueset_value

认为 这在单线程嵌入式 Rust 中应该是完全安全的,但我一直无法找到明确的答案。我只对不需要分配或销毁的类型感兴趣(例如此处示例中的原始类型)。

我能看到的唯一问题是编译器或处理器以意想不到的方式重新排序访问(这可以使用易失性访问方法解决),但 unsafe 本身 ?


编辑:

The book 表明这是安全的,只要我们可以保证没有多线程数据竞争(显然是这里的情况)

With mutable data that is globally accessible, it’s difficult to ensure there are no data races, which is why Rust considers mutable static variables to be unsafe.

The docs 措辞不太明确,表明数据竞争只是这可能不安全的一种方式,但不会扩展到其他示例

accessing mutable statics can cause undefined behavior in a number of ways, for example due to data races in a multithreaded context

The nomicon 建议这应该是安全的,只要您不以某种方式取消引用错误的指针。

可变静态通常是不安全的,因为它们规避了正常的借用检查规则,这些规则强制执行恰好存在 1 个可变借用或存在任意数量的不可变借用(包括 0),这允许您编写导致未定义行为的代码。例如,以下编译并打印 2 2:

static mut COUNTER: i32 = 0;

fn main() {
    unsafe {
        let mut_ref1 = &mut COUNTER;
        let mut_ref2 = &mut COUNTER;
        *mut_ref1 += 1;
        *mut_ref2 += 1;
        println!("{mut_ref1} {mut_ref2}");
    }
}

但是我们有两个可变引用同时存在于内存中的同一位置,即 UB。

我相信您在此处发布的代码是安全的,但我通常不建议使用 static mut。使用原子,SyncUnsafeCell/UnsafeCell,围绕实现 SyncCell 的包装器,这是安全的,因为您的环境是 single-threaded,或者老实说几乎任何东西别的。 static mut 非常 不安全,强烈建议不要使用它。

在这种情况下,它并非不可靠,但您仍应避免使用它,因为 it is too easy to misuse it in a way that is UB.

而是使用 UnsafeCell 的包装器,即 Sync:

pub struct SyncCell<T>(UnsafeCell<T>);

unsafe impl<T> Sync for SyncCell<T> {}

impl<T> SyncCell<T> {
    pub const fn new(v: T) -> Self { Self(UnsafeCell::new(v)); }

    pub unsafe fn set(&self, v: T) { *self.0.get() = v; }
}

impl<T: Copy> SyncCell<T> {
    pub unsafe fn get(&self) -> T { *self.0.get() }
}

如果每晚使用,可以使用SyncUnsafeCell

请注意,只要启用中断,就没有 single-threaded 代码之类的东西。所以即使对于微控制器,可变静态也是不安全的。

如果您真的可以保证 single-threaded 访问,那么您的假设是正确的,即访问原始类型应该是安全的。这就是 Cell 类型存在的原因,它允许原始类型的可变性,但它不是 Sync (这意味着它明确地阻止了线程访问)。

也就是说,要创建一个安全的静态变量,它需要实现 Sync 正是出于上述原因;由于显而易见的原因,Cell 不这样做。

为了在不使用不安全块的情况下实际拥有一个原始类型的可变全局变量,我个人会使用 AtomicAtomics 不分配并且在 core 库中可用,这意味着它们在微控制器上工作。

use core::sync::atomic::{AtomicUsize, Ordering};

static MY_VALUE: AtomicUsize = AtomicUsize::new(0);

pub fn set_value(val: usize) {
    MY_VALUE.store(val, Ordering::Relaxed)
}

pub fn get_value() -> usize {
    MY_VALUE.load(Ordering::Relaxed)
}

fn main() {
    println!("{}", get_value());
    set_value(42);
    println!("{}", get_value());
}

具有 Relaxed 的原子是 zero-overhead on almost all architectures

为了回避如何在 single-threaded 代码中安全使用可变静态变量的问题,另一种选择是使用 thread-local storage:

use std::cell::Cell;

thread_local! (static MY_VALUE: Cell<usize> = {
    Cell::new(0)
});

pub fn set_value(val: usize) {
    MY_VALUE.with(|cell| cell.set(val))
}

pub fn get_value() -> usize {
    MY_VALUE.with(|cell| cell.get())
}