Rust 的所有權和借用系統讓全域變數的處理方式與其他語言有所不同。直接使用 static mut 雖然可以建立可變的全域變數,但需要 unsafe 區塊,並不推薦。本文介紹了幾種更安全的全域狀態管理方案,例如使用 lazy_staticonce_cellstatic_init 等函式庫來實作延遲初始化,避免在編譯時期初始化帶有堆積積分配的資料結構。這些函式庫也提供了執行緒安全的機制,例如使用 Mutex 或原子型別,以防止資料競爭。此外,文章還討論了使用 std::sync::OnceLockstd::cell::OnceCell 等標準函式庫工具來確保全域變數只被初始化一次,並示範瞭如何在多執行緒環境下安全地存取全域狀態。

4.6 全域狀態管理

在開發者的生涯中,總會遇到需要處理全域狀態(Global State)的時候。雖然我們通常會盡量避免使用全域狀態,因為它可能引入競爭條件(Race Conditions)、資料損壞風險(Corruption Risk)、以及其他問題,但實際開發中卻難以完全避免。本文將討論在 Rust 中處理全域狀態的策略,這些策略既受到 Rust 記憶體和所有權模型的限制,也受到其安全特性的影響。

使用靜態變數與常數

Rust 只允許兩種全域變數:staticconst。這兩種變數的值都必須在編譯時確定,也就是說,不能在執行時初始化全域變數。可以定義可變的靜態變數,但這種做法被視為不安全,需要使用 unsafe 關鍵字。此外,靜態變數必須是 Sync 型別,以確保執行緒安全,避免競爭條件。同時,靜態變數不允許動態分配記憶體(即不能使用堆積上的資料結構),並且在程式結束時,不會呼叫 drop() 方法釋放資源。

程式碼範例:嘗試建立靜態向量

static POPULAR_BABY_NAMES_2021: Vec<String> = vec![
    String::from("Olivia"),
    String::from("Liam"),
    String::from("Emma"),
    String::from("Noah"),
];

內容解密:

上述程式碼無法編譯透過,因為 VecString 都是動態分配記憶體的資料結構,不允許在靜態變數中使用。編譯器會報錯,指出在靜態變數中不允許記憶體分配。

錯誤訊息如下:

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! 巨集來建立執行緒區域性的靜態變數,並結合 ArcMutex 來實作執行緒安全的分享資料。

程式碼範例:使用 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.rsonce_cellstatic_init。這些函式庫提供了更簡潔和安全的 API 來實作全域狀態的延遲初始化。

常見全域狀態管理函式庫

函式庫名稱倉函式庫地址下載次數(截至2024年3月3日)描述
lazy-static.rshttps://mng.bz/oegy215,759,981提供巨集來宣告延遲初始化的靜態變數
once_cellhttps://github.com/matklad/once_cell213,996,727提供新的 cell-like 型別,用於初始化全域狀態
static_inithttps://gitlab.com/okannen/static_init3,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_staticonce_cellstatic_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);
}

內容解密:

  1. 使用 lazy_static!巨集定義一個名為 POPULAR_BABY_NAMES_2020 的全域變數。
  2. 該變數是一個 Vec<String>,包含了一些熱門的嬰兒名字。
  3. 使用 * 運算元來存取 POPULAR_BABY_NAMES_2020 的值,因為 lazy_static 實作了 Deref 特徵。

使用 once_cell 管理全域狀態

once_cell 是另一個提供懶初始化功能的套件。相比於 lazy_staticonce_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);
}

內容解密:

  1. 使用 Lazy::new 建立一個懶初始化的全域變數 POPULAR_BABY_NAMES_2019
  2. 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);
}

內容解密:

  1. 使用 #[dynamic] 屬性巨集來定義一個動態初始化的全域變數。
  2. 同樣地,使用 * 來存取其值,因為 static_init 也提供了 Deref 特徵的實作。

使用標準函式庫中的 std::cell::OnceCell

Rust 的標準函式庫提供了 std::cell::OnceCellstd::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());
}

內容解密:

  1. 建立一個 OnceCell 例項來儲存嬰兒名字的向量。
  2. 使用 get_or_init 方法來初始化或取得已初始化的值。
  3. 使用 get 方法來取得儲存的值。