Rust 的所有權和借用系統讓全域變數的處理方式與其他語言有所不同。直接使用 static mut
雖然可以建立可變的全域變數,但需要 unsafe
區塊,並不推薦。本文介紹了幾種更安全的全域狀態管理方案,例如使用 lazy_static
、once_cell
和 static_init
等函式庫來實作延遲初始化,避免在編譯時期初始化帶有堆積積分配的資料結構。這些函式庫也提供了執行緒安全的機制,例如使用 Mutex
或原子型別,以防止資料競爭。此外,文章還討論了使用 std::sync::OnceLock
和 std::cell::OnceCell
等標準函式庫工具來確保全域變數只被初始化一次,並示範瞭如何在多執行緒環境下安全地存取全域狀態。
4.6 全域狀態管理
在開發者的生涯中,總會遇到需要處理全域狀態(Global State)的時候。雖然我們通常會盡量避免使用全域狀態,因為它可能引入競爭條件(Race Conditions)、資料損壞風險(Corruption Risk)、以及其他問題,但實際開發中卻難以完全避免。本文將討論在 Rust 中處理全域狀態的策略,這些策略既受到 Rust 記憶體和所有權模型的限制,也受到其安全特性的影響。
使用靜態變數與常數
Rust 只允許兩種全域變數:static
和 const
。這兩種變數的值都必須在編譯時確定,也就是說,不能在執行時初始化全域變數。可以定義可變的靜態變數,但這種做法被視為不安全,需要使用 unsafe
關鍵字。此外,靜態變數必須是 Sync
型別,以確保執行緒安全,避免競爭條件。同時,靜態變數不允許動態分配記憶體(即不能使用堆積上的資料結構),並且在程式結束時,不會呼叫 drop()
方法釋放資源。
程式碼範例:嘗試建立靜態向量
static POPULAR_BABY_NAMES_2021: Vec<String> = vec![
String::from("Olivia"),
String::from("Liam"),
String::from("Emma"),
String::from("Noah"),
];
內容解密:
上述程式碼無法編譯透過,因為 Vec
和 String
都是動態分配記憶體的資料結構,不允許在靜態變數中使用。編譯器會報錯,指出在靜態變數中不允許記憶體分配。
錯誤訊息如下:
error[E0010]: allocations are not allowed in statics
--> src/main.rs:1:47
|
1 | static POPULAR_BABY_NAMES_2021: Vec<String> = vec![
| _______________________________________________^
2 | | String::from("Olivia"),
3 | | String::from("Liam"),
4 | | String::from("Emma"),
5 | | String::from("Noah"),
6 | | ];
| |_^ allocation not allowed in statics
手動實作延遲初始化
為了實作全域狀態的延遲初始化,可以使用 std::thread_local!
巨集來建立執行緒區域性的靜態變數,並結合 Arc
和 Mutex
來實作執行緒安全的分享資料。
程式碼範例:使用 thread_local!
和 Arc<Mutex<Option<Vec<String>>>>
use std::sync::{Arc, Mutex};
use std::thread;
thread_local! {
static POPULAR_BABY_NAMES_2021: Arc<Mutex<Option<Vec<String>>>> =
Arc::new(Mutex::new(None));
}
fn main() {
let arc = POPULAR_BABY_NAMES_2021.with(|arc| arc.clone());
let mut inner = arc.lock().expect("unable to lock mutex");
*inner = Some(vec![
String::from("Olivia"),
String::from("Liam"),
String::from("Emma"),
String::from("Noah"),
]);
}
內容解密:
這段程式碼定義了一個執行緒區域性的靜態變數 POPULAR_BABY_NAMES_2021
,它包含一個 Arc
包裹的 Mutex
,用於儲存一個可選的 Vec<String>
。在 main
函式中,我們對這個變數進行初始化。這種方法雖然可行,但實務上較為麻煩,且需要小心處理初始化順序。
使用現成的函式庫
在實際開發中,更推薦使用現成的函式庫來處理全域狀態,例如 lazy-static.rs
、once_cell
和 static_init
。這些函式庫提供了更簡潔和安全的 API 來實作全域狀態的延遲初始化。
常見全域狀態管理函式庫
函式庫名稱 | 倉函式庫地址 | 下載次數(截至2024年3月3日) | 描述 |
---|---|---|---|
lazy-static.rs | https://mng.bz/oegy | 215,759,981 | 提供巨集來宣告延遲初始化的靜態變數 |
once_cell | https://github.com/matklad/once_cell | 213,996,727 | 提供新的 cell-like 型別,用於初始化全域狀態 |
static_init | https://gitlab.com/okannen/static_init | 3,391,550 | 提供高效能的全域靜態變數初始化,並支援資料丟棄 |
使用 lazy-static.rs
lazy-static.rs
是目前最流行的解決 Rust 全域狀態問題的方法。它的 API 根據一個簡單的巨集,使用 static ref
來宣告延遲初始化的靜態變數。
程式碼範例:使用 lazy_static!
巨集
use lazy_static::lazy_static;
use std::sync::{Arc, Mutex};
lazy_static! {
static ref POPULAR_BABY_NAMES_2021: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![
String::from("Olivia"),
String::from("Liam"),
String::from("Emma"),
String::from("Noah"),
]));
}
fn main() {
let names = POPULAR_BABY_NAMES_2021.lock().unwrap();
println!("{:?}", names);
}
內容解密:
這段程式碼使用 lazy_static!
巨集定義了一個名為 POPULAR_BABY_NAMES_2021
的靜態變數,它是一個包含 Vec<String>
的 Arc<Mutex>
。這個變數在第一次被存取時初始化,並且是執行緒安全的。在 main
函式中,我們鎖定這個 Mutex 並列印出向量中的內容。
深入理解 Rust 中的全域狀態管理
在 Rust 程式設計中,全域狀態的管理是一個重要的課題。適當地管理全域狀態可以提升程式的可維護性和效能。本文將探討 Rust 中處理全域狀態的幾種方法,包括使用 lazy_static
、once_cell
、static_init
以及標準函式庫中的 std::cell::OnceCell
。
為什麼需要全域狀態?
在某些情況下,我們需要在程式的多個部分分享某些資料或狀態。全域狀態提供了一種方便的方式來實作這種分享。然而,全域狀態的使用也需要謹慎,因為它可能導致程式的耦合度增加和可測試性下降。
使用 lazy_static
管理全域狀態
lazy_static
是一個流行的 Rust 套件,它允許我們定義在第一次存取時才初始化的全域變數。這種懶初始化(lazy initialization)機制可以避免不必要的計算和資源浪費。
程式碼範例:使用 lazy_static
定義全域變數
use lazy_static::lazy_static;
lazy_static! {
static ref POPULAR_BABY_NAMES_2020: Vec<String> = {
vec![
String::from("Olivia"),
String::from("Liam"),
String::from("Emma"),
String::from("Noah"),
]
};
}
fn main() {
println!("popular baby names of 2020: {:?}", *POPULAR_BABY_NAMES_2020);
}
內容解密:
- 使用
lazy_static!
巨集定義一個名為POPULAR_BABY_NAMES_2020
的全域變數。 - 該變數是一個
Vec<String>
,包含了一些熱門的嬰兒名字。 - 使用
*
運算元來存取POPULAR_BABY_NAMES_2020
的值,因為lazy_static
實作了Deref
特徵。
使用 once_cell
管理全域狀態
once_cell
是另一個提供懶初始化功能的套件。相比於 lazy_static
,once_cell
提供了更通用的 API。
程式碼範例:使用 once_cell
定義全域變數
use once_cell::sync::Lazy;
static POPULAR_BABY_NAMES_2019: Lazy<Vec<String>> = Lazy::new(|| {
vec![
String::from("Olivia"),
String::from("Liam"),
String::from("Emma"),
String::from("Noah"),
]
});
fn main() {
println!("popular baby names of 2019: {:?}", *POPULAR_BABY_NAMES_2019);
}
內容解密:
- 使用
Lazy::new
建立一個懶初始化的全域變數POPULAR_BABY_NAMES_2019
。 Lazy
型別同樣實作了Deref
特徵,因此可以使用*
來存取其值。
使用 static_init
管理全域狀態
static_init
提供了一種不同的方式來初始化全域變數,具有優秀的效能。
程式碼範例:使用 static_init
定義全域變數
use static_init::dynamic;
#[dynamic]
static POPULAR_BABY_NAMES_2018: Vec<String> = vec![
String::from("Emma"),
String::from("Liam"),
String::from("Olivia"),
String::from("Noah"),
];
fn main() {
println!("popular baby names of 2018: {:?}", *POPULAR_BABY_NAMES_2018);
}
內容解密:
- 使用
#[dynamic]
屬性巨集來定義一個動態初始化的全域變數。 - 同樣地,使用
*
來存取其值,因為static_init
也提供了Deref
特徵的實作。
使用標準函式庫中的 std::cell::OnceCell
Rust 的標準函式庫提供了 std::cell::OnceCell
和 std::sync::OnceLock
來處理全域狀態的初始化問題。雖然它們不像上述套件那樣提供懶初始化的功能,但仍然很有用。
程式碼範例:使用 std::cell::OnceCell
use std::cell::OnceCell;
fn main() {
let popular_baby_names_2017: OnceCell<Vec<String>> = OnceCell::new();
popular_baby_names_2017.get_or_init(|| {
vec![
String::from("Emma"),
String::from("Liam"),
String::from("Olivia"),
String::from("Noah"),
]
});
println!("{:?}", popular_baby_names_2017.get());
}
內容解密:
- 建立一個
OnceCell
例項來儲存嬰兒名字的向量。 - 使用
get_or_init
方法來初始化或取得已初始化的值。 - 使用
get
方法來取得儲存的值。