Rust 的 lazy_static 模式

靜態數據是計算機編程中的一個基本概念,指的是存儲在全局內存空間中並在程序的整個生命週期中持續存在的數據。在 Rust 中,靜態數據用於存儲程序中所有線程共享的值,並保證在使用前進行初始化。然而,Rust 中有各種形式的靜態數據,其中一種叫做 lazy_static。

lazy_static 是 Rust 中的一種模式:惰性初始化靜態數據,其中值僅在第一次線程安全訪問時初始化,這與常規靜態數據 (在編譯時初始化) 形成對比。惰性靜態值以線程安全的方式初始化,可用於存儲全局變量或共享常量數據。

在本文中,我們將探索 Rust 中的 lazy_static 概念及其各種用途。我們將看看 lazy_static 是如何工作的,使用它的優點和缺點,以及如何在 Rust 項目中使用它的一些實際示例。

lazy_static 是如何工作的?

爲了在 Rust 中使用 lazy_static,你需要在你的項目中包含 lazy_static 庫。這個庫提供了一個名爲 lazy_static! 的宏,這允許你定義一個 lazy_static 變量。下面是一個如何聲明 lazy_static 變量的例子:

use lazy_static::lazy_static;

lazy_static! {
    static ref MY_VAR: String = "some value".to_string();
}

如你所見,lazy_static! 宏接受一個定義 lazy_static 的代碼塊。在本例中,我們定義了一個名爲 MY_VAR 的靜態變量,它的類型爲 String,初始化值爲 “some value”。

當 MY_VAR 第一次被訪問時,它將被初始化爲值 “some value”。後續訪問將返回初始化的值,而無需重新初始化它。這就是 lazy_static 值與常規靜態數據不同的地方,後者在編譯時初始化,不能在運行時更改。

要訪問 lazy_static 值,可以使用與訪問其他靜態變量相同的語法。例如:

fn main() {
    println!("My lazy static value is: {}", *MY_VAR);
}

需要注意的是,lazy_static 值存儲在堆中,而不是堆棧中。這意味着它們遵循與堆分配數據相同的規則,例如當不再需要它們時需要釋放它們。但是,因爲 lazy_static 值只初始化一次,並且在所有線程之間共享,所以可以有效地訪問它們,而不需要重複分配和釋放。

在 Rust 中使用 lazy_static

現在我們已經瞭解了 lazy_static 是如何工作的,讓我們來探索在 Rust 項目中如何使用它。

線程安全的全局變量

使用 lazy_static 的主要好處之一是能夠存儲線程安全的全局變量。因爲惰性靜態值是以線程安全的方式初始化的,所以可以從多個線程安全地訪問它們,而不需要額外的同步。在希望避免鎖定和解鎖共享資源開銷的情況下,這可能特別有用。

例如,考慮一個具有多個線程的程序,這些線程需要訪問一個共享的計數器變量。如果沒有 lazy_static,你需要使用互斥來同步對計數器的訪問:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }
    for handle in handles {
        handle.join().unwrap();
    }
    println!("Result: {}", *counter.lock().unwrap());
}

在這個例子中,我們使用了一個 Arc(原子引用計數) 和一個互斥鎖來同步訪問計數器。這工作得很好,但是它以每次訪問計數器時鎖定和解鎖互斥鎖的形式給程序增加了開銷。

然而,我們可以使用 lazy_static 將計數器存儲爲全局變量,從而避免同步的需要:

use lazy_static::lazy_static;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;

lazy_static! {
    static ref COUNTER: AtomicUsize = AtomicUsize::new(0);
}

fn main() {
    let mut handles = Vec::new();

    for _ in 0..10 {
        let handle = thread::spawn(|| {
            COUNTER.fetch_add(1, Ordering::SeqCst);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", COUNTER.load(Ordering::SeqCst));
}

在本例中,我們使用 AtomicUsize 作爲 lazy_static 變量來存儲計數器。這允許我們在計數器上執行原子操作,例如 fetch_add,它以線程安全的方式將計數器增加給定值。因爲計數器存儲爲全局變量,並通過 lazy_static! 宏訪問,所以我們不需要擔心同步或鎖定和解鎖互斥量的開銷。

共享常量數據

lazy_static 在存儲共享常量數據時也很有用。因爲該值只初始化一次,所以可以有效地訪問它,而不需要重複計算。這可以提高程序的性能,特別是當該值的計算成本很高時。

例如,考慮一個需要高精度計算圓周率值的程序。這可能是一項計算成本很高的任務,特別是當程序需要多次計算 pi 的值時。爲了避免這種開銷,我們可以使用 lazy_static 將 pi 的計算值存儲爲一個全局常量:

use lazy_static::lazy_static;

lazy_static! {
    static ref PI: f64 = compute_pi();
}

fn compute_pi() -> f64 {
    // expensive computation to determine the value of pi
    3.14159265358979323846
}

fn main() {
    println!("The value of pi is: {}", *PI);
}

在這個例子中,當 lazy_static 變量第一次被訪問時,pi 的值只計算了一次。對 pi 值的後續訪問將返回初始化的值,而無需重新計算它。這可以避免多次執行昂貴的 pi 計算,提高了程序的性能。

性能優化

除了存儲線程安全的全局變量和共享常量數據外,lazy_static 還可以優化 Rust 程序的性能。通過在實際需要時才初始化數據,lazy_static 可以幫助減少程序的內存和計算開銷。

例如,考慮一個具有僅在某些情況下才需要的大型數據結構的程序。如果沒有 lazy_static,我們可能會在程序開始時初始化數據結構,即使它在程序的大部分執行中並不需要:

fn main() {
    let data = initialize_data();

    if condition {
        use_data(&data);
    }
}

fn initialize_data() -> Vec<i32> {
    // expensive operation to initialize data structure
    vec![1, 2, 3, 4, 5]
}

fn use_data(data: &Vec<i32>) {
    // use the data structure
}

在本例中,數據結構在程序開始時初始化,即使不需要它。如果 condition 條件不滿足且數據結構很大,這可能是浪費,因爲它會給程序增加不必要的內存和計算開銷。

爲了避免這種開銷,我們可以使用 lazy_static 來延遲數據結構的初始化,直到實際需要時:

use lazy_static::lazy_static;

lazy_static! {
    static ref DATA: Vec<i32> = initialize_data();
}

fn main() {
    if condition {
        use_data(&*DATA);
    }
}

fn initialize_data() -> Vec<i32> {
    // expensive operation to initialize data structure
    vec![1, 2, 3, 4, 5]
}

fn use_data(data: &Vec<i32>) {
    // use the data structure
}

在本例中,只有在滿足條件並訪問 data 變量時才初始化數據結構。這可以避免不必要的數據結構初始化來幫助減少程序的內存和計算開銷。

使用 lazy_static 的優點和缺點

雖然 lazy_static 在 Rust 中是一個有用的工具,但是瞭解使用它的優點和缺點是很重要的。

lazy_static 的主要優點之一是能夠存儲線程安全的全局變量和共享常量數據。正如我們在前面的例子中看到的,lazy_static 可以通過避免同步和重複計算的開銷來幫助提高程序的性能。它使用起來也相對簡單,具有簡單易懂的語法。

然而,使用 lazy_static 也有一些限制。一個潛在的問題是可能會出現在初始化時產生競爭條件,其中多個線程可能試圖同時初始化相同的 lazy_static 值。爲了避免這種情況,可以使用 once_cell 庫,它提供了一個線程安全的單元格,只能初始化一次。

lazy_static 的另一個缺點是它增加了程序的複雜性。通過使用它,你爲你的程序添加了一個額外的抽象層,這對其他開發人員來說可能不是很明顯。這可能會使理解和調試程序更加困難,特別是如果你不熟悉 lazy_static 庫的話。

lazy_static 的替代方案

正如前面提到的,lazy_static 的一個潛在問題是可能存在初始化競爭條件,其中多個線程可能試圖同時初始化相同的 lazy_static 值。爲了避免這種情況,我們可以使用 once_cell 庫。

once_cell 庫

once_cell 庫提供了一個名爲 OnceCell 的類型,它是一個只能初始化一次的單個值的容器。一旦在 OnceCell 中初始化了一個值,就可以從多個線程安全地訪問它,而不需要額外的同步。

下面是一個如何在 Rust 中使用 OnceCell 的例子:

use once_cell::sync::OnceCell;

static DATA: OnceCell<Vec<i32>> = OnceCell::new();

fn main() {
    let data = DATA.get_or_init(|| vec![1, 2, 3, 4, 5]);
    println!("Data: {:?}", data);
}

在本例中,我們使用 OnceCell 的 get_or_init 方法用整數向量的值初始化 DATA 變量。如果 DATA 已經初始化,get_or_init 將簡單地返回初始化的值。這可以確保 DATA 只初始化一次,即使是從多個線程訪問它。

LazyLock 庫

lazy_static 的另一個替代方法是 LazyLock,它是一個提供線程安全的惰性初始化器的庫。像 lazy_static 一樣,LazyLock 允許你定義一個僅在第一次訪問時才初始化的值。然而,與 lazy_static 不同的是,LazyLock 使用鎖來同步對值的訪問,確保它只初始化一次,即使存在多個線程訪問它。

下面是一個如何在 Rust 中使用 LazyLock 的例子:

use lazy_lock::LazyLock;

lazy_lock::lazy_lock! {
    static DATA: Vec<i32> = vec![1, 2, 3, 4, 5];
}

fn main() {
    let data = DATA.lock().unwrap();
    println!("Data: {:?}", data);
}

在本例中,我們使用 lazy_lock! 宏來定義一個名爲 DATA 的 LazyLock 變量。當訪問 DATA 變量時,使用互斥鎖鎖定它,以確保它只初始化一次。這有助於避免初始化競爭條件,並允許從多個線程安全訪問 DATA。

lazy_static、OnceCell 和 LazyLock 的區別

這些變量之間的一個關鍵區別是它們處理初始化競爭條件的方式。Lazy_static 不提供任何同步機制,因此多個線程可以同時嘗試初始化相同的 Lazy_static 值。這可能導致競爭條件和未定義的行爲。另一方面,OnceCell 和 LazyLock 都使用同步機制來確保值只初始化一次,即使存在多個線程同時訪問。

另一個區別是你對初始化過程的控制級別。對於 lazy_static,你可以使用宏定義初始化值,並且在第一次訪問時自動初始化該值。使用 OnceCell,你可以對初始化過程有更多的控制,因爲你可以指定一個閉包,如果值還沒有初始化,則調用該閉包來初始化它。如果初始化過程更復雜或涉及昂貴的計算,這可能很有用。LazyLock 也允許你爲初始化指定一個閉包,類似於 OnceCell。

就性能而言,由於缺乏同步開銷,lazy_static 可能比其他兩個選項更有優勢。因爲 lazy_static 不使用鎖或其他同步機制,在某些情況下,它可能比 OnceCell 和 LazyLock 更快。但是,重要的是要注意,這將取決於具體的用例和初始化過程的開銷。

總的來說,在 lazy_static、OnceCell 和 LazyLock 之間的選擇取決於你的特定需求和程序的要求。如果需要存儲線程安全的全局變量或共享常量數據,並且願意接受初始化競爭條件的潛在風險,那麼 lazy_static 可能是一個不錯的選擇。另一方面,如果你需要確保值只初始化一次,並且願意接受增加的同步開銷,那麼 OnceCell 或 LazyLock 可能是更好的選擇。

總結

總之,lazy_static 是 Rust 中用於存儲線程安全的全局變量和共享常量數據的有用工具。它可以通過避免重複計算以及鎖定和解鎖共享資源的開銷來提高程序的性能。然而,重要的是要意識到存在初始化競爭條件的可能性,並使用適當的措施來避免它們。

總的來說,是否在 Rust 程序中使用 lazy_static 是在性能改進的好處和它所帶來的額外複雜性之間的權衡。如果你正在處理一個需要線程安全的全局變量或共享常量數據的項目,那麼 lazy_static 可能是一個可以考慮的有用工具。另一方面,如果你正在編寫一個簡單的程序,而不需要使用 lazy_static 的好處,那麼最好還是使用常規的靜態數據。

與任何編程工具一樣,瞭解 lazy_static 的功能和限制並在項目中適當地使用它是很重要的。通過揭開 lazy_static 的概念及其用途的神祕面紗,我們可以就何時以及如何在 Rust 程序中使用它做出明智的決定。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/HyIOSST_FnFEHsNx8wjrPA