齐娅晶 发表于 2025-6-5 15:20:48

rust学习二十.3、RUST使用不安全代码访问静态变量

一、前言

1.1、为什么要使用不安全块访问可变静态变量

根据rust设计人的理解:静态变量是可以修改,所以在并发情况下是可能存在并发/并行时候的不一致问题(如果要修改),这可能就不安全了。
所以,rust规定访问可变静态变量需要使用不安全代码块(unsafe块)。
 
1.2、比较静态变量和常量

1.常量与不可变静态变量的一个微妙的区别是静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。
常量则允许在任何被用到的时候复制其数据,具体而言应该是内联了(在用到的地方替换进行替换)。
2.另一个区别在于静态变量可以是可变的。访问和修改可变静态变量都是不安全的
1.3、多线程修改静态变量

在多线程中,访问静态变量(写)必须使用互斥锁。其次已经不能使用Arc(原子引用计数指针)来存储它了,必须使用延迟锁指针(LazyLock)来保存,否则无法编译.
还有一种方式更加复杂,就是还是使用Arc,但是需要使用到OnceLock(一次性锁指针)。
LazyLock - 延时锁指针/延迟锁
延迟锁特性:

[*]需要简单的延迟初始化
[*]初始化逻辑固定且不会失败的场景
[*]可以使用 nightly Rust 的项目
[*]性能要求较高的场景(因为少了一些运行时检查)
OnceLock - 一次性锁指针/一次性锁

[*]需要显式控制初始化时机的场景
[*]需要处理初始化失败的场景
[*]需要在 stable Rust 中使用的项目- 对安全性要求较高的场景
二、示例

2.1、示例-单线程静态和多线程中的LazyLock

use std::sync::{Mutex,LazyLock};
use std::thread;
use std::io;
use std::io::Write;

//通常静态变量会定义在全局作用域中,并且通常是大写字母开头的常量命名规则。
static mut COUNTER: u32 = 0;
static mut ARC_COUNTER:LazyLock<Mutex<u32>> = LazyLock::new(|| Mutex::new(0));
static mut SIGN:u32 = 0;

//允许指向静态变量的共享引用,不要一直提示了
#
fn main() {
    //1.0 演示单线程下unsafe访问全局静态变量
    access_static_in_singlethread();
   
    //2.0 演示局部静态变量 -- 在方法/函数,代码块内都可以定义static,只是作用范围不同而已
    define_partitional_static();

    //3.0 演示多线程下对全局变量的访问   
    print!("请输入线程数量:");//注意print!宏不会刷新输出流,除非有换行符,所以需要手动刷新输出流
    std::io::stdout().flush().unwrap();
    let thread_qty = read_thread_qty();
    unsafe{
      change_static_in_parallel(thread_qty);
    }   
    unsafe {
      println!("主线程-ARC_COUNTER现在的值: {}",*ARC_COUNTER.lock().unwrap());
      println!("--- 预备.... 开始!!! ");
      //通知所有子线程开始执行
      SIGN=1;
      //在主线程中循环打印ARC_COUNTER的值,直到其值达到10
      while *ARC_COUNTER.lock().unwrap() < thread_qty {
            println!("主线程-ARC_COUNTER现在的值: {}",*ARC_COUNTER.lock().unwrap());
            thread::sleep(std::time::Duration::from_millis(5));
      }
      //打印最终的ARC_COUNTER的值
      println!("主线程-ARC_COUNTER最后的值: {}",*ARC_COUNTER.lock().unwrap());
    }
}
#
fn access_static_in_singlethread() {
    unsafe {
      println!("COUNTER(开始前): {}",COUNTER);
    }
    let add_qty: u32 = 3;
    add_to_count(add_qty);
    unsafe {
      println!("COUNTER(+{}后): {}",add_qty,COUNTER);
    }
}
fn add_to_count(inc: u32) {
    unsafe {
      COUNTER += inc;
    }
}

/**
* 演示多线程下对全局变量的访问
*/
#
unsafe fn change_static_in_parallel(thread_qty:u32) {
    //这里仅仅是一个复制,还是不能修改COUNTER
    //利用懒惰指针+互斥锁
    let mut handles = vec![];
    for _ in 0..thread_qty {
      let handle = thread::spawn(|| {
            //等待主线程通知开始执行子线程任务,否则可能有的很快就跑完了,导致主线程还没开始就结束了
            while SIGN==0 {
                thread::sleep(std::time::Duration::from_millis(50));
            }
            let mut num=ARC_COUNTER.lock().unwrap();
            *num += 1;
            println!("----子线程{:?}+1得到{}", thread::current().id(),*num);
      });
      handles.push(handle);
    }
    //此处不要join,因为它要等待SIGN=1后才执行,且就算SIGN=1,也会被主线程阻塞
}

fn define_partitional_static() {
    //但如果你愿意,也可以定义一个局部静态变量。
    static HELLO_WORLD: &str = "Hello, world!";
    println!("{}", HELLO_WORLD);
}

fnread_thread_qty() -> u32 {
    let mut gts=String::new();
        io::stdin().read_line(&mut gts).expect("读取失败");
        let gts: u32=gts.trim().parse().expect("请输入一个数字");
    return gts;
}代码中有两个例子:
1.定义普通的静态变量,并在单个线程中修改它
2.定义可变静态变量,在多线程中修改
输出结果:

输入线程数后,继续执行:

证实了,在多线程中,的确可以修改静态变量。
2.2、示例-多线程中的OnceLock

再来一个OnceLock的例子:
use std::io;
use std::io::Write;
use std::sync::{Arc, Mutex, OnceLock};
use std::thread;

//通常静态变量会定义在全局作用域中,并且通常是大写字母开头的常量命名规则。
static ARC_AGE: OnceLock>> = OnceLock::new();
static mut SIGN: u32 = 0;

//允许指向静态变量的共享引用,不要一直提示了
#
fn main() {
    ARC_AGE.get_or_init(|| Arc::new(Mutex::new(18))); // 初始年龄设为18
    print!("请输入线程数量:"); //注意print!宏不会刷新输出流,除非有换行符,所以需要手动刷新输出流
    std::io::stdout().flush().unwrap();
    let thread_qty = read_thread_qty();
    // 重置SIGN为0,准备执行新的示例
    println!("\n现在开始演示ARC_AGE的并行修改:");

    // 执行新的示例
    change_static_in_parallel2(thread_qty);

    let age = ARC_AGE.get().unwrap();
    println!("主线程-ARC_AGE现在的值: {}", *age.lock().unwrap());
    println!("--- 预备.... 开始!!! ");

    unsafe {
      SIGN = 1;
    }

    // 在主线程中循环打印ARC_AGE的值,直到其值达到目标值
    let target_age = 18 + thread_qty;
    while *age.lock().unwrap() < target_age {
      println!("主线程-ARC_AGE现在的值: {}", *age.lock().unwrap());
      thread::sleep(std::time::Duration::from_millis(5));
    }
    println!("主线程-ARC_AGE最后的值: {}", *age.lock().unwrap());
}

/**
* 使用Arc演示多线程下对全局变量的访问
*/
fn change_static_in_parallel2(thread_qty: u32) {
    let age = ARC_AGE.get().unwrap().clone();
    let mut handles = vec![];

    for _ in 0..thread_qty {
      let age = age.clone();
      let handle = thread::spawn(move || {
            //等待主线程通知开始执行子线程任务
            unsafe {
                while SIGN == 0 {
                  thread::sleep(std::time::Duration::from_millis(50));
                }
            }
            let mut current_age = age.lock().unwrap();
            *current_age += 1;
            println!(
                "----子线程{:?}将年龄+1得到{}",
                thread::current().id(),
                *current_age
            );
      });
      handles.push(handle);
    }
    //此处不要join,因为它要等待SIGN=1后才执行,且就算SIGN=1,也会被主线程阻塞
}

fn read_thread_qty() -> u32 {
    let mut gts = String::new();
    io::stdin().read_line(&mut gts).expect("读取失败");
    let gts: u32 = gts.trim().parse().expect("请输入一个数字");
    return gts;
}执行结果如下:

需要注意的是,OnceLock不需要在安全代码块中,这一点和LazyLock很不同。
三、小结


[*]静态变量是一个好东西,那个语言都少不了
[*]但是如果要在多线程中修改静态变量,则可能需要使用不安全代码访问(LazyLock),也可能不需要(OnceLock)

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: rust学习二十.3、RUST使用不安全代码访问静态变量