一、前言
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;
//允許指向靜態變量的共享引用,不要一直提示了
#[allow(static_mut_refs)]
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());
}
}
#[allow(static_mut_refs)]
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;
}
}
/**
* 演示多線程下對全局變量的訪問
*/
#[allow(static_mut_refs)]
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);
}
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;
}
代碼中有兩個例子:
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<Arc<Mutex<u32>>> = OnceLock::new();
static mut SIGN: u32 = 0;
//允許指向靜態變量的共享引用,不要一直提示了
#[allow(static_mut_refs)]
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)