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

问题描述 投票:0回答:5

我正在开发单核嵌入式芯片。在 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_value
set_value

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

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

unsafe
本身


编辑:

这本书表明,只要我们能保证没有多线程数据竞争(显然就是这里的情况),这就是安全的

对于全局可访问的可变数据,很难确保不存在数据争用,这就是 Rust 认为可变静态变量不安全的原因。

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

访问可变静态可能会以多种方式导致未定义的行为,例如由于多线程上下文中的数据争用

经济学表明,只要您不以某种方式取消引用坏指针,这应该是安全的。

rust embedded unsafe
5个回答
14
投票

请注意,只要启用了中断,就不存在单线程代码。因此,即使对于微控制器,可变静态也是不安全的。

如果您确实可以保证单线程访问,那么您的假设是正确的,即访问原始类型应该是安全的。这就是

Cell
类型存在的原因,它允许原始类型的可变性,但它不是
Sync
(意味着它明确阻止线程访问)。

也就是说,要创建一个安全的静态变量,它需要实现

Sync
,正是出于上述原因;由于显而易见的原因,
Cell
不这样做。

要真正拥有一个具有原始类型的可变全局变量而不使用不安全块,我个人会使用

Atomic
Atomic
不分配并且可在
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
的原子在几乎所有架构上都是零开销


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() }
}

如果每晚使用,可以使用

SyncUnsafeCell


2
投票

可变静态通常是不安全的,因为它们规避了正常的借用检查器规则,这些规则强制存在恰好 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
,一个围绕实现
Cell
Sync
的包装器,这是安全的,因为您的环境是单线程的,或者说实话,它只是其他任何东西。
static mut
非常不安全,强烈建议不要使用它。


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())
}

0
投票

这是函数内可变静态变量必须是原子的证明。

use lazy_static::lazy_static;
use std::sync::Mutex;
use rayon::prelude::*;

fn increment_counter() -> (i32, i32) {
    lazy_static! {
        static ref SAFE_COUNTER: Mutex<i32> = Mutex::new(0);
    }

    // mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior
    static mut UNSAFE_COUNTER: i32 = 0;
    unsafe {
        UNSAFE_COUNTER += 1;
    } 
    
    let mut safe_counter = SAFE_COUNTER.lock().unwrap();
    *safe_counter += 1;

    unsafe {
        (*safe_counter, UNSAFE_COUNTER)
    }
}

fn main() {
    let n: i32 = 1000000;
    (0..n-1).into_par_iter().for_each(|_| {
        increment_counter();
    });
    let res = increment_counter();
    println!("thread-safe: {}, not-thread-safe: {}", res.0, res.1);
}

此输出表明,函数内的静态变量默认情况下不是线程安全的,但程序员必须将不安全的静态变量标记到

unsafe
子句中,从而获得保护。

thread-safe: 1000000, not-thread-safe: 953687
© www.soinside.com 2019 - 2024. All rights reserved.