如果在单线程环境下,可变的静态基元类型是否实际上是“不安全”的?

11

我正在开发单核嵌入式芯片。在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中完全安全,但我找不到明确的答案。我只关心不需要分配或销毁(如此处示例中的原语)的类型。 唯一的注意事项是编译器或处理器以意外的方式重新排序访问(可以使用volatile访问方法解决),但这本质上是否不安全? 编辑: 该书建议只要我们能保证没有多线程数据竞争(在这里显然是这种情况),这样就可以安全地进行操作。 对于全局可访问的可变数据,很难确保没有数据竞争,这就是为什么Rust认为可变静态变量是不安全的原因。 文档的措辞没那么明确,它只是暗示访问可变静态变量可能有多种不安全的方式,例如在多线程上下文中由于数据竞争而导致未定义行为。 Nomicon认为,只要你不以某种方式解引用错误的指针,这就应该是安全的。

1
@ChayimFriedman 我认为在某种意义上使用“unsafe”是很普遍的,意思是“这段被unsafe包装的代码不是一个有效的安全抽象”。我相信我以前经常看到它被用在那个上下文中。对我来说,“unsound”可能包括更广泛的正确性问题。但如果我搞混了,请指点一下参考资料。 - JMAA
1
完备性是一种逻辑属性。如果编译器无法证明Rust代码块的完备性,则该代码块被认为是unsafe的。 - Ian S.
1
这是 Rust 文档中 unsafeunsound 的官方描述:https://doc.rust-lang.org/beta/reference/behavior-considered-undefined.html - Finomnis
3
我认为你思维过程中最关键的错误是认为单线程微控制器不会涉及并发问题。中断也是一种导致与多线程完全相同的问题的并发形式,并且它们存在于几乎所有微控制器上。 - Finomnis
@finomnis 谢谢,显然我需要提高我的阅读能力,因为我已经在那个页面上了 :P 另外,有关中断的观点很好。 - JMAA
显示剩余5条评论
4个回答

12
请注意,只要中断被启用,就不存在单线程代码这样的东西。因此,即使对于微控制器,可变静态变量也是不安全的。
如果您确实可以保证单线程访问,则您的假设是正确的,访问基本类型应该是安全的。这就是为什么存在 Cell 类型,它允许使用基本类型进行可变性,但不能使用 Sync(这意味着它明确地防止了线程访问)。
也就是说,要创建一个安全的静态变量,它需要实现 Sync,原因正如上述所述;而 Cell 由于显而易见的原因没有这样做。
要实际上有一个可变的全局变量,使用原始类型而不使用不安全块,我个人会使用 AtomicAtomic 不分配并且在 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 的原子操作在几乎所有架构上都是 零开销 的。


经过在我的特定案例(armv4)中测试后,不幸的是Ordering::Relaxed是我唯一的选项。使用acqurie / release会出现链接器错误(可能是由于缺乏相关内部函数引起的,但这与本问题无关)。 - JMAA
1
请注意,如果架构上不存在相应的 mov 命令,则原子操作可能无法正常工作,例如,在 32 位系统上,AtomicU128 不是无锁的,因此可能无法编译。 - Finomnis
2
Cell 还有一个让它听起来不错的事实,除了它是 !Sync 之外:它不允许用户从对单元格本身的共享引用中获取任何内部引用。即使在线程安全方面分开考虑,允许这样的访问也会使其不可靠。 - trent
这在你和原帖的例子中都是成立的,但它并没有明确说明。 - trent
@Finomnis 感谢您的关注。据我所知,如果我的代码是100%单线程的,则某些rust健全性保证是多余而昂贵的。在我的情况下,我使用全局静态伪随机数生成器状态,并且它非常重要,因此需要直接修改。我可能会尝试一些其他模式,但是实现这个让我想知道是否有一种方法可以说这个代码是安全的,没有线程和中断回调。也许 Cell 就是我需要的,但它似乎需要一个复制-更新-写入模式。 - THK
显示剩余7条评论

4
在这种情况下,它并不是不合理的,但你仍然应该避免使用它,因为它太容易被误用,导致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() }
}

如果您使用的是nightly版本,则可以使用SyncUnsafeCell

在这里使用UnsafeCell相比Cell有什么优势吗? - hkBst
1
@hkBst 不完全是。 UnsafeCell 是所有其他原语基于的基本内部可变性基元。但您可以将 Cellunsafe impl Sync 包装起来使用。 - Chayim Friedman
你介意详细说明一下这是怎么做到的吗?对我来说,你的代码听起来很像包含竞争条件...没有任何东西阻止两个线程同时调用“set”或“set”和“get”,所以我不认为这个结构体应该是“同步”的。 - Finomnis
@Finomnis,OP正在谈论一种情况,其中所有内容都是单线程的。 - Chayim Friedman
我猜他随后必须手动确保不从中断处理程序中调用它? - Finomnis

2

可变静态变量通常不安全,因为它们绕过了正常的借用检查规则,这些规则强制执行只存在一个可变借用或任意数量的不可变借用(包括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}");
    }
}

然而,当我们有两个可变引用同时存在于同一内存位置时,这就是未定义行为。
我相信您发布的代码是安全的,但通常不建议使用“static mut”。使用原子类型、SyncUnsafeCell/UnsafeCell、包装器(wrapper)包含实现Sync的Cell,因为您的环境是单线程的,或者其他任何东西都可以。Static mut非常不安全,强烈不建议使用。

谢谢。这回答了标题的问题,但没有回答我问题主体中的代码(由于构造原因,不允许这样的别名)。 - JMAA
1
编辑以特别回复您的代码示例 - Ian S.
"static mut 非常不安全..." 或许我的问题最好改为 "只要我使用基本类型并避免别名和线程,它就是安全的吗?"。也许我以后会写一个更清晰的问题。 - JMAA
当然,我通常会使用原子操作、互斥锁等,但并非每个平台都支持这些操作,也不是每种用例都能承受这种开销。我会更深入地研究 UsafeCell 等技术。 - JMAA
1
如果你做得正确(确保遵守借用规则,这基本上涵盖了你提到的所有内容),那么它是可靠的,逻辑上是正确的。关键是很容易搞砸,其他抽象(例如Chayim提到的UnsafeCellSyncUnsafeCell)可用于更明确地表示不安全性,并具有更明确地概述所选择的合同的文档。我的个人建议是基于Chayim的答案或类似地只是包装Cell来构建你的解决方案,因为你的类型是Copy - Ian S.

0
为了回避可变静态变量在单线程代码中如何安全使用的问题,另一个选择是使用 线程本地存储
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())
}

(a) 对于这个问题,CellRefCell 更好。 (b) OP 表示他们正在开发嵌入式设备,因此可能没有 std 或线程本地变量。 - Chayim Friedman
@ChayimFriedman,两个好建议,谢谢!我解决了(a),并接受(b)作为一种限制。我仍然认为这对于非嵌入式系统很有用。 - hkBst

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接