Arc
你還記得我們用 Rc
來給予變數一個以上的所有者。如果我們執行緒中做一樣的事情,我們則需要 Arc
。Arc
的意思是 "原子參考計數器(atomic reference counter)"。原子的意思是它使用計算機的處理器,所以資料每回只會被寫入一次。這點很重要,因為如果兩個執行緒同時寫入資料,你會得到錯誤的結果。例如,想像如果你能在 Rust 中做到這一點:
#![allow(unused)] fn main() { // 🚧 let mut x = 10; for i in 0..10 { // 執行緒 1 x += 1 } for i in 0..10 { // 執行緒 2 x += 1 } }
如果執行緒 1 和執行緒 2 一起啟動,也許就會出現這種情況:
- 執行緒 1 看到 10,寫入 11,接著執行緒 2 看到 11,寫入 12。到目前為止沒有問題。
- 執行緒 1 看到 12。同時,執行緒 2 看到 12。執行緒 1,寫入 13。執行緒 2 也寫入 13。現在我們有 13,但應該要是 14。這是個大問題。
Arc
使用處理器來確保這種情況不會發生,所以當你有多個執行緒時這個方法你就必須使用。然而你不會想在單執行緒上用 Arc
,因為 Rc
更快一些。
不過你不能只用 Arc
來改變資料。所以你要用 Mutex
把資料包起來,然後再用 Arc
把 Mutex
包起來。
所以讓我們用 Mutex
來在 Arc
裡面改變數字的值。首先讓我們設定一個執行緒:
fn main() { let handle = std::thread::spawn(|| { println!("The thread is working!") // 只測試執行緒 }); handle.join().unwrap(); // 讓執行緒在這等待直到完成 println!("Exiting the program"); }
目前為止只印出:
The thread is working!
Exiting the program
很好。現在讓我們把它放在 for
迴圈中,跑 0..5
。
fn main() { let handle = std::thread::spawn(|| { for _ in 0..5 { println!("The thread is working!") } }); handle.join().unwrap(); println!("Exiting the program"); }
這也是可行的。我們得到以下結果:
The thread is working!
The thread is working!
The thread is working!
The thread is working!
The thread is working!
Exiting the program
現在讓我們再多加一個執行緒。每個執行緒都會做同樣的事情。你可以看到這些執行緒是同時工作的。有時會先印出 Thread 1 is working!
,但其他時候是 Thread 2 is working!
先印出。這就是所謂的並行(concurrency),也就是 "一起執行"的意思。
fn main() { let thread1 = std::thread::spawn(|| { for _ in 0..5 { println!("Thread 1 is working!") } }); let thread2 = std::thread::spawn(|| { for _ in 0..5 { println!("Thread 2 is working!") } }); thread1.join().unwrap(); thread2.join().unwrap(); println!("Exiting the program"); }
會列印:
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Exiting the program
現在我們要改變 my_number
的數值。現在它是 i32
。我們將把它改為 Arc<Mutex<i32>>
:由 Arc
保護可以改變的 i32
。
#![allow(unused)] fn main() { // 🚧 let my_number = Arc::new(Mutex::new(0)); }
現在我們有了這個,我們可以克隆它。每個克隆可以進入不同的執行緒。我們有兩個執行緒,所以我們將做兩個克隆:
#![allow(unused)] fn main() { // 🚧 let my_number = Arc::new(Mutex::new(0)); let my_number1 = Arc::clone(&my_number); // 這個克隆去到執行緒 1 let my_number2 = Arc::clone(&my_number); // 這個克隆去到執行緒 2 }
現在,我們已把安全的克隆附加到 my_number
,我們可以將它們 move
到其它執行緒中沒問題。
use std::sync::{Arc, Mutex}; fn main() { let my_number = Arc::new(Mutex::new(0)); let my_number1 = Arc::clone(&my_number); let my_number2 = Arc::clone(&my_number); let thread1 = std::thread::spawn(move || { // 只有克隆去到執行緒 1 for _ in 0..10 { *my_number1.lock().unwrap() +=1; // 鎖住 Mutex, 改值 } }); let thread2 = std::thread::spawn(move || { // 只有克隆去到執行緒 2 for _ in 0..10 { *my_number2.lock().unwrap() += 1; } }); thread1.join().unwrap(); thread2.join().unwrap(); println!("Value is: {:?}", my_number); println!("Exiting the program"); }
程式印出:
Value is: Mutex { data: 20 }
Exiting the program
所以它成功了。
接著我們可以將兩個執行緒一起合併(join)到一個 for
迴圈裡,並使程式碼更短。
我們需要儲存控制碼(handle),這樣我們就可以在迴圈外對每個執行緒呼叫 .join()
。如果我們在迴圈內這樣做,它將等待第一個執行緒完成後再啟動新的執行緒。
use std::sync::{Arc, Mutex}; fn main() { let my_number = Arc::new(Mutex::new(0)); let mut handle_vec = vec![]; // JoinHandles 將會放在這 for _ in 0..2 { // 做兩次 let my_number_clone = Arc::clone(&my_number); // 在啟動執行緒前做出克隆 let handle = std::thread::spawn(move || { // 移入克隆 for _ in 0..10 { *my_number_clone.lock().unwrap() += 1; } }); handle_vec.push(handle); // 儲存控制碼我們才能在迴圈外對它呼叫 join // 如果我們不把它推入向量, 它將會直接死在這 } handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // 對所有控制碼呼叫 join println!("{:?}", my_number); }
最後印出 Mutex { data: 20 }
。
這看起來很複雜,但 Arc<Mutex<SomeType>>>
在 Rust 中非常頻繁的被使用,所以它變得很自然。另外,你也可以隨時把你的程式碼寫得更乾淨。這裡是同樣的程式碼,多了一行 use
敘述和兩個函式。這些函式並沒有做任何新的事情,但是它們把一些程式碼從 main()
中移出。如果很難讀懂的話,你可以嘗試重寫這樣的程式碼。
use std::sync::{Arc, Mutex}; use std::thread::spawn; // 現在我們只需要寫 spawn fn make_arc(number: i32) -> Arc<Mutex<i32>> { // 只是用來做 Arc 裡有 Mutex 的函式 Arc::new(Mutex::new(number)) } fn new_clone(input: &Arc<Mutex<i32>>) -> Arc<Mutex<i32>> { // 只是讓我們可以寫成 new_clone 的函式 Arc::clone(&input) } // 現在 main() 更容易閱讀了 fn main() { let mut handle_vec = vec![]; // 每個控制碼將會放到這裡 let my_number = make_arc(0); for _ in 0..2 { let my_number_clone = new_clone(&my_number); let handle = spawn(move || { for _ in 0..10 { let mut value_inside = my_number_clone.lock().unwrap(); *value_inside += 1; } }); handle_vec.push(handle); // 拿到控制碼了, 所以放進向量裡 } handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // 讓每一個等待 println!("{:?}", my_number); }