Rust 併發編程 - 容器類併發原語
Rust 在併發編程方面有一些強大的原語,讓你能夠寫出安全且高效的併發代碼。最顯著的原語之一是 ownership system,它允許你在沒有鎖的情況下管理內存訪問。此外,Rust 還提供了一些併發編程的工具和標準庫, 比如線程、線程池、消息通訊 (mpsc 等)、原子操作等,不過這一章我們不介紹這些工具和庫,它們會專門的分章節去講。這一章我們專門講一些保證在線程間共享的一些方式和庫。
併發原語內容較多,分成兩章,這一章介紹Cow
、beef::Cow
、Box
、 Cell
、RefCell
、OnceCell
、LazyCell
、LazyLock
和 Rc
。我把它們稱之爲容器類併發原語,主要基於它們的行爲,它們主要是對普通數據進行包裝,以便提供其他更豐富的功能。
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
,可以自動解引用,比如直接調用len
和into
:
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::borrowed
、Cow::from
、Cow::owned
,標準庫Cow
也有這三個方法,它們的區別是:
-
borrowed
: 借用已有資源 -
from
: 從已有資源複製創建 Owned -
owned
: 自己提供資源內容
這個例子下半部分對比了標準庫Cow
和beef::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
Cell
和RefCell
是 Rust 中用於內部可變性 (interior mutability) 的兩個重要類型。
Cell
和RefCell
都是可共享的可變容器。可共享的可變容器的存在是爲了以受控的方式允許可變性,即使存在別名引用。Cell 和 RefCell 都允許在單線程環境下以這種方式進行。然而,無論是 Cell 還是 RefCell 都不是線程安全的(它們沒有實現 Sync)。
Cell
Cell<T>
允許在不違反借用規則的前提下, 修改其包含的值:
-
Cell
中的值不再擁有所有權, 只能通過get
和set
方法訪問。 -
set
方法可以在不獲取可變引用的情況下修改Cell
的值。 -
適用於簡單的單值容器,如整數或字符。
下面這個例子創建了一個 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>
提供了更靈活的內部可變性,允許在運行時檢查借用規則, 通過運行時借用檢查來實現:
-
通過
borrow
和borrow_mut
方法進行不可變和可變借用。 -
借用必須在作用域結束前歸還, 否則會 panic。
-
適用於包含多個字段的容器。
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
確保其內部值只能被寫入一次。一旦值被寫入,後續的寫入操作將被忽略。 -
懶初始化:
OnceCell
支持懶初始化,這意味着它只有在需要時纔會進行初始化。這在需要在運行時確定何時初始化值的情況下很有用。 -
線程安全:
OnceCell
提供了線程安全的一次性寫入。在多線程環境中,它確保只有一個線程能夠成功寫入值,而其他線程的寫入嘗試將被忽略。
下面這個例子演示了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
使用引用計數來追蹤指向數據的引用數量。當引用計數降爲零時,數據會被自動釋放。 -
Rc
允許多個 Rc 指針共享相同的數據,而無需擔心所有權的轉移。 -
Rc
內部存儲的數據是不可變的。如果需要可變性,可以使用 RefCell 或 Mutex 等內部可變性的機制。 -
Rc
在處理循環引用時需要額外注意,因爲循環引用會導致引用計數無法降爲零,從而導致內存泄漏。爲了解決這個問題,可以使用 Weak 類型。
下面這個例子演示了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