RefCell 在多執行緒
本集目標
理解為什麼 interior mutability 在多執行緒下是危險的,以及 RefCell 的 Send / Sync 特性。
概念說明
interior mutability 是多執行緒的一大威脅
第 5 章學過,RefCell 能透過 &T(共享參考)修改內部的值。在單執行緒的世界裡,RefCell 會在執行時期檢查借用規則,不會出問題。
但在多執行緒的世界裡,事情就不一樣了。&T 看起來是「只讀」的,而 Sync 的定義就是 &T 能安全地在多個執行緒之間共享。如果一個型別能透過 &T 偷偷修改內容,多個執行緒同時這樣做就可能出事。
RefCell 的借用計數不是 atomic
RefCell 用普通的整數來記錄目前的借用狀態(有幾個不可變借用、有沒有可變借用)。這個計數器的操作不是 atomic 的。atomic 操作是不可分割的——其他執行緒不可能看到做到一半的狀態。RefCell 的計數器沒有這種保證,所以兩個執行緒可能都在任一方更新前讀到相同的舊值。如果兩個執行緒同時透過 &RefCell<T> 呼叫 borrow_mut(),以下是可能發生的事:
- 執行緒 A 呼叫
borrow_mut(),讀取計數器,看到值是 0(沒有人在借)。 - 執行緒 B 也呼叫
borrow_mut(),讀取計數器,也看到值是 0。 - 執行緒 A 判斷「沒有人在借,可以拿可變借用」,把計數器改成「可變借用中」。
- 執行緒 B 也判斷「沒有人在借」——因為它在步驟 2 讀到的是舊值——也拿到了可變借用。
結果:兩個執行緒同時拿到了可變借用,RefCell 的執行時期檢查完全被繞過了。
RefCell 不是 Sync
因為上面的原因,RefCell 不是 Sync——&RefCell<T> 不能在多個執行緒之間共享。如果你試著這樣做,編譯器會擋住你。
當 T: Send 時,RefCell<T> 是 Send
只要 T 本身是 Send,RefCell<T> 就可以被 move 到另一個執行緒。move 之後,只有那一個執行緒擁有這個 RefCell,不存在多個執行緒同時操作的問題。
use std::cell::RefCell;
use std::thread;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// OK:RefCell<Vec<i32>> 是 Send,可以 move 到另一個執行緒
let handle = thread::spawn(move || {
data.borrow_mut().push(4);
println!("{:?}", data.borrow());
});
handle.join().expect("執行緒發生錯誤");
}
範例程式碼
use std::cell::RefCell;
use std::thread;
fn main() {
// RefCell<String> 可以 move 到另一個執行緒(Send)
let data = RefCell::new(String::from("hello"));
let handle = thread::spawn(move || {
// 在這個執行緒裡,RefCell 運作正常
data.borrow_mut().push_str(" world");
println!("子執行緒:{}", data.borrow());
});
handle.join().expect("執行緒發生錯誤");
// 但不能在多個執行緒之間共享 &RefCell(非 Sync)
// 如果你試著讓兩個執行緒共享同一個 RefCell,編譯器會擋住你。
}
重點整理
- interior mutability 讓
&T能修改內容,但這在多執行緒下很危險。 - atomic 操作 = 不可分割的操作,其他執行緒不可能看到做到一半的狀態。
RefCell的borrow計數是普通整數,不是 atomic,多執行緒同時操作可能繞過檢查。RefCell不是Sync——不能在多個執行緒之間共享&RefCell<T>。- 當
T: Send時,RefCell<T>是Send——可以 move 到另一個執行緒,因為 move 後只有一個執行緒擁有。