Rust 併發編程 - 容器類併發原語

Rust 在併發編程方面有一些強大的原語,讓你能夠寫出安全且高效的併發代碼。最顯著的原語之一是 ownership system,它允許你在沒有鎖的情況下管理內存訪問。此外,Rust 還提供了一些併發編程的工具和標準庫, 比如線程、線程池、消息通訊 (mpsc 等)、原子操作等,不過這一章我們不介紹這些工具和庫,它們會專門的分章節去講。這一章我們專門講一些保證在線程間共享的一些方式和庫。

併發原語內容較多,分成兩章,這一章介紹Cowbeef::CowBoxCellRefCellOnceCellLazyCellLazyLockRc。我把它們稱之爲容器類併發原語,主要基於它們的行爲,它們主要是對普通數據進行包裝,以便提供其他更豐富的功能。

Cow

Cow 不是 🐄,而是clone-on-write或者copy-on-write的縮寫。

Cow(Copy-on-write) 是一種優化內存和提高性能的技術, 通常應用在資源共享的場景。

其基本思想是, 當有多個調用者 (callers) 同時請求相同的資源時, 都會共享同一份資源, 直到有調用者試圖修改資源內容時, 系統纔會真正複製一份副本出來給該調用者, 而其他調用者仍然使用原來的資源。

Rust 中的 String 和 Vec 等類型就利用了 Cow。例如:

let s1 = String::from("hello");
let s2 = s1; // s1和s2共享同一份內存

s2.push_str(" world"); // s2會進行寫操作,於是系統複製一份新的內存給s2

這樣可以避免大量未修改的字符串、向量等的重複分配和複製, 提高內存利用率和性能。

cow 的優點是:

缺點是:

需要根據實際場景權衡使用。但對於存在大量相同或相似資源的共享情況, 使用 cow 可以帶來顯著性能提升。

標準庫中std::borrow::Cow 類型是一個智能指針,提供了寫時克隆(clone-on-write)的功能:它可以封裝並提供對借用數據的不可變訪問,當需要進行修改或獲取所有權時,它可以惰性地克隆數據。

Cow 實現了Deref,這意味着你可以直接在其封裝的數據上調用不可變方法。如果需要進行改變,則 to_mut 將獲取到一個對擁有的值的可變引用,必要時進行克隆。

下面的代碼將origin字符串包裝成一個cow, 你可以把它 borrowed 成一個&str, 其實也可以直接在cow調用&str方法,因爲Cow實現了Deref,可以自動解引用,比如直接調用leninto

    let origin = "hello world";
    let mut cow = Cow::from(origin);
    assert_eq!(cow, "hello world");

    // Cow can be borrowed as a str
    let s: &str = &cow;
    assert_eq!(s, "hello world");

    assert_eq!(s.len(), cow.len());

    // Cow can be converted to a String
    let s: String = cow.into();
    assert_eq!(s, "HELLO WORLD");

接下來我們以一個寫時 clone 的例子。下面這個例子將字符串中的字符全部改成大寫字母:

    // Cow can be borrowed as a mut str
    let s: &mut str = cow.to_mut();
    s.make_ascii_uppercase();
    assert_eq!(s, "HELLO WORLD");
    assert_eq!(origin, "hello world");

這裏使用to_mut得到一個可變引用,一旦 s 有修改,它會從原始數據中 clone 一份,在克隆的數據上進行修改。

所以如果你想在某些數據上實現copy-on-write/clone-on-write的功能,可以考慮使用std::borrow::Cow

更進一步,beef庫提供了一個更快,更緊湊的Cow類型, 它的使用方法和標準庫的Cow使用方法類似:

pub fn beef_cow() {
    let borrowed: beef::Cow<str> = beef::Cow::borrowed("Hello");
    let owned: beef::Cow<str> = beef::Cow::owned(String::from("World"));
    let _ = beef::Cow::from("Hello");

    assert_eq!(format!("{} {}!", borrowed, owned)"Hello World!",);

    const WORD: usize = size_of::<usize>();

    assert_eq!(size_of::<std::borrow::Cow<str>>(), 3 * WORD);
    assert_eq!(size_of::<beef::Cow<str>>(), 3 * WORD);
    assert_eq!(size_of::<beef::lean::Cow<str>>(), 2 * WORD);
}

這個例子的上半部分演示了生成beef::Cow的三種方法Cow::borrowedCow::fromCow::owned,標準庫Cow也有這三個方法,它們的區別是:

這個例子下半部分對比了標準庫Cowbeef::Cow以及更緊湊的beef::lean::Cow所佔內存的大小。可以看到對於數據是str類型的 Cow,現在的標準庫的Cow佔三個 WORD, 和 beef::Cow 相當, 而進一步壓縮的 beef::lean::Cow 只佔了兩個 Word。

cow-utils針對字符串的 Cow 做了優化,性能更好。

Box

Box<T>,通常簡稱爲box,提供了在 Rust 中最簡單的堆分配形式。Box 爲這個分配提供了所有權,並在超出作用域時釋放其內容。Box 還確保它們不會分配超過 isize::MAX 字節的內存。

它的使用很簡單,下面的例子就是把值val從棧上移動到堆上:

let val: u8 = 5;
let boxed: Box<u8> = Box::new(val);

那麼怎麼反其道而行之呢?下面的例子就是通過解引用把值從堆上移動到棧上:

let boxed: Box<u8> = Box::new(5);
let val: u8 = *boxed;

如果我們要定義一個遞歸的數據結構,比如鏈表,下面的方式是不行的,因爲 List 的大小不固定,我們不知道該分配給它多少內存:

#[derive(Debug)]
enum List<T> {
    Cons(T, List<T>),
    Nil,
}

這個時候就可以使用Box了:

#[derive(Debug)]
enum List<T> {
    Cons(T, Box<List<T>>),
    Nil,
}

let list: List<i32> = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
println!("{list:?}");

目前 Rust 還提供一個實驗性的類型ThinBox, 它是一個瘦指針,不管內部元素的類型是啥:

pub fn thin_box_example() {
    use std::mem::{size_of, size_of_val};
    let size_of_ptr = size_of::<*const ()>();

    let box_five = Box::new(5);
    let box_slice = Box::<[i32]>::new_zeroed_slice(5);
    assert_eq!(size_of_ptr, size_of_val(&box_five));
    assert_eq!(size_of_ptr * 2, size_of_val(&box_slice));


    let five = ThinBox::new(5);
    let thin_slice = ThinBox::<[i32]>::new_unsize([1, 2, 3, 4]);
    assert_eq!(size_of_ptr, size_of_val(&five));
    assert_eq!(size_of_ptr, size_of_val(&thin_slice));
}

Cell、RefCell、OnceCell、LazyCell 和 LazyLock

CellRefCell是 Rust 中用於內部可變性 (interior mutability) 的兩個重要類型。

CellRefCell都是可共享的可變容器。可共享的可變容器的存在是爲了以受控的方式允許可變性,即使存在別名引用。Cell 和 RefCell 都允許在單線程環境下以這種方式進行。然而,無論是 Cell 還是 RefCell 都不是線程安全的(它們沒有實現 Sync)。

Cell

Cell<T>允許在不違反借用規則的前提下, 修改其包含的值:

下面這個例子創建了一個 Cell, 賦值給變量x, 注意 x 是不可變的,但是我們能夠通過set方法修改它的值,並且即使存在對 x 的引用 y 時也可以修改它的值:

use std::cell::Cell;

let x = Cell::new(42);
let y = &x;

x.set(10); // 可以修改

println!("y: {:?}", y.get());  // 輸出 y: 10

RefCell

RefCell<T> 提供了更靈活的內部可變性,允許在運行時檢查借用規則, 通過運行時借用檢查來實現:

use std::cell::RefCell;

let x = RefCell::new(42);

{
    let y = x.borrow();
    // 在這個作用域內,只能獲得不可變引用
    println!("y: {:?}", *y.borrow());
}

{
    let mut z = x.borrow_mut();
    // 在這個作用域內,可以獲得可變引用
    *z = 10;
}

println!("x: {:?}", x.borrow().deref());

如果你開啓了#![feature(cell_update)], 你還可以更新它:c.update(|x| x + 1);

OnceCell

OnceCell 是 Rust 標準庫中的一個類型,用於提供一次性寫入的單元格。它允許在運行時將值放入單元格,但只允許一次。一旦值被寫入,進一步的寫入嘗試將被忽略。

主要特點和用途:

下面這個例子演示了OnceCell使用方法,還未初始化的時候,獲取的它的值是 None, 一旦初始化爲Hello, World!, 它的值就固定下來了:

pub fn once_cell_example() {
    let cell = OnceCell::new();
    assert!(cell.get().is_none()); // true

    let value: &String = cell.get_or_init(|| "Hello, World!".to_string());
    assert_eq!(value, "Hello, World!");
    assert!(cell.get().is_some()); //true
}

LazyCell、LazyLock

有時候我們想實現懶 (惰性) 初始化的效果,當然lazy_static庫可以實現這個效果,但是 Rust 標準庫也提供了一個功能,不過目前還處於不穩定的狀態,你需要設置#![feature(lazy_cell)]使能它。

下面是一個使用它的例子:

#![feature(lazy_cell)]

use std::cell::LazyCell;

let lazy: LazyCell<i32> = LazyCell::new(|| {
    println!("initializing");
    46
});
println!("ready");
println!("{}", *lazy); // 46
println!("{}", *lazy); // 46

注意它是懶初始化的,也就是你在第一次訪問它的時候它纔會調用初始化函數進行初始化。

但是它不是線程安全的,如果想使用線程安全的版本,你可以使用std::sync::LazyLock:

use std::collections::HashMap;

use std::sync::LazyLock;

static HASHMAP: LazyLock<HashMap<i32, String>> = LazyLock::new(|| {
    println!("initializing");
    let mut m = HashMap::new();
    m.insert(13, "Spica".to_string());
    m.insert(74, "Hoyten".to_string());
    m
});

fn main() {
    println!("ready");
    std::thread::spawn(|| {
        println!("{:?}", HASHMAP.get(&13));
    }).join().unwrap();
    println!("{:?}", HASHMAP.get(&74));
}

rc

Rc 是 Rust 標準庫中的一個智能指針類型,全名是 std::rc::Rc,代表 "reference counting"。它用於在多個地方共享相同數據時,通過引用計數來進行所有權管理。

下面這個例子演示了Rc的基本使用方法,通過clone我們可以獲得新的共享引用。

use std::rc::Rc;

let data = Rc::new(42);

let reference1 = Rc::clone(&data);
let reference2 = Rc::clone(&data);

// data 的引用計數現在爲 3

// 當 reference1 和 reference2 被丟棄時,引用計數減少

注意Rc 允許在多個地方共享不可變數據,通過引用計數來管理所有權。

如果還想修改數據,那麼就可以使用上一節的Cell相關類型, 比如下面的例子,我們使用Rc<RefCell<HashMap>>類型來實現這個需求:

pub fn rc_refcell_example() {
    let shared_map: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::new()));
    {
        let mut map: RefMut<_> = shared_map.borrow_mut();
        map.insert("africa", 92388);
        map.insert("kyoto", 11837);
        map.insert("piccadilly", 11826);
        map.insert("marbles", 38);
    }

    let total: i32 = shared_map.borrow().values().sum();
    println!("{total}");
}

這樣我們就針對不可變類型Rc實現了數據的可變性。

注意Rc不是線程安全的,針對上面的裏面,如果想實現線程安全的類型,你可以使用Arc, 不過這個類型我們放在下一章進行再介紹。

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