詳解Rust編程中的共享狀態(tài)并發(fā)執(zhí)行
1.共享狀態(tài)并發(fā)
雖然消息傳遞是一個很好的處理并發(fā)的方式,但并不是唯一一個。另一種方式是讓多個線程擁有相同的共享數(shù)據(jù)。在學(xué)習(xí)Go語言編程過程中大家應(yīng)該聽到過一句口號:"不要通過共享內(nèi)存來通訊"。
在某種程度上,任何編程語言中的信道都類似于單所有權(quán),因為一旦將一個值傳送到信道中,將無法再使用這個值。共享內(nèi)存類似于多所有權(quán):多個線程可以同時訪問相同的內(nèi)存位置。第十五章介紹了智能指針如何使得多所有權(quán)成為可能,然而這會增加額外的復(fù)雜性,因為需要以某種方式管理這些不同的所有者。Rust 的類型系統(tǒng)和所有權(quán)規(guī)則極大的協(xié)助了正確地管理這些所有權(quán)。作為一個例子,讓我們看看互斥器,一個更為常見的共享內(nèi)存并發(fā)原語。
互斥器(mutex)是 mutual exclusion 的縮寫,也就是說,任意時刻,其只允許一個線程訪問某些數(shù)據(jù)。為了訪問互斥器中的數(shù)據(jù),線程首先需要通過獲取互斥器的 鎖(lock)來表明其希望訪問數(shù)據(jù)。鎖是一個作為互斥器一部分的數(shù)據(jù)結(jié)構(gòu),它記錄誰有數(shù)據(jù)的排他訪問權(quán)。因此,我們描述互斥器為通過鎖系統(tǒng) 保護(guarding)其數(shù)據(jù)。
互斥器以難以使用著稱,因為你不得不記住:
- 在使用數(shù)據(jù)之前嘗試獲取鎖。
- 處理完被互斥器所保護的數(shù)據(jù)之后,必須解鎖數(shù)據(jù),這樣其他線程才能夠獲取鎖。
作為一個現(xiàn)實中互斥器的例子,想象一下在某個會議的一次小組座談會中,只有一個麥克風(fēng)。如果一位成員要發(fā)言,他必須請求或表示希望使用麥克風(fēng)。一旦得到了麥克風(fēng),他可以暢所欲言,然后將麥克風(fēng)交給下一位希望講話的成員。如果一位成員結(jié)束發(fā)言后忘記將麥克風(fēng)交還,其他人將無法發(fā)言。如果對共享麥克風(fēng)的管理出現(xiàn)了問題,座談會將無法如期進(jìn)行!
正確的管理互斥器異常復(fù)雜,這也是許多人之所以熱衷于信道的原因。然而,在 Rust 中,得益于類型系統(tǒng)和所有權(quán),我們不會在鎖和解鎖上出錯。
2.Mutex<T>的API
作為展示如何使用互斥器的例子,讓我們從在單線程上下文使用互斥器開始, 看下面的代碼:
use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); *num = 6; } println!("m = {:?}", m); }
像很多類型一樣,我們使用關(guān)聯(lián)函數(shù) new
來創(chuàng)建一個 Mutex<T>
。使用 lock
方法獲取鎖,以訪問互斥器中的數(shù)據(jù)。這個調(diào)用會阻塞當(dāng)前線程,直到我們擁有鎖為止。
如果另一個線程擁有鎖,并且那個線程 panic 了,則 lock
調(diào)用會失敗。在這種情況下,沒人能夠再獲取鎖,所以這里選擇 unwrap
并在遇到這種情況時使線程 panic。
一旦獲取了鎖,就可以將返回值(在這里是num
)視為一個其內(nèi)部數(shù)據(jù)的可變引用了。類型系統(tǒng)確保了我們在使用 m
中的值之前獲取鎖。m
的類型是 Mutex<i32>
而不是 i32
,所以 必須 獲取鎖才能使用這個 i32
值。我們是不會忘記這么做的,因為反之類型系統(tǒng)不允許訪問內(nèi)部的 i32
值。
Mutex<T>
是一個智能指針。更準(zhǔn)確的說,lock
調(diào)用 返回 一個叫做 MutexGuard
的智能指針。這個智能指針實現(xiàn)了 Deref
來指向其內(nèi)部數(shù)據(jù);其也提供了一個 Drop
實現(xiàn)當(dāng) MutexGuard
離開作用域時自動釋放鎖,為此,我們不會忘記釋放鎖并阻塞互斥器為其它線程所用的風(fēng)險,因為鎖的釋放是自動發(fā)生的。
丟棄了鎖之后,可以打印出互斥器的值,并發(fā)現(xiàn)能夠?qū)⑵鋬?nèi)部的 i32
改為 6。
3.在線程間共享Mutex<T>
現(xiàn)在讓我們嘗試使用 Mutex<T>
在多個線程間共享值。我們將啟動十個線程,并在各個線程中對同一個計數(shù)器值加一,這樣計數(shù)器將從 0 變?yōu)?10??聪旅娴拇a:
use std::sync::Mutex; use std::thread; fn main() { let counter = Mutex::new(0); let mut handles = vec![]; for _ in 0..10 { 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()); }
這里創(chuàng)建了一個 counter
變量來存放內(nèi)含 i32
的 Mutex<T>
, 接下來遍歷 range 創(chuàng)建了 10 個線程。使用了 thread::spawn
并對所有線程使用了相同的閉包:它們每一個都將調(diào)用 lock
方法來獲取 Mutex<T>
上的鎖,接著將互斥器中的值加一。當(dāng)一個線程結(jié)束執(zhí)行,num
會離開閉包作用域并釋放鎖,這樣另一個線程就可以獲取它了。
在主線程中,我們收集了所有的 join 句柄, 調(diào)用它們的 join
方法來確保所有線程都會結(jié)束。這時,主線程會獲取鎖并打印出程序的結(jié)果。
編譯上面的代碼, Rust編譯器報了一個錯誤:
錯誤信息表明 counter
值在上一次循環(huán)中被移動了。所以 Rust 告訴我們不能將 counter
鎖的所有權(quán)移動到多個線程中。下面來看看如何修復(fù)這個錯誤。
4.多線程和多所有權(quán)
我們先嘗試將Mutex<T>封裝進(jìn)Rc<T>中并在將所有權(quán)移入線程之前克隆Rc<T>,看下面代碼:
use std::rc::Rc; use std::sync::Mutex; use std::thread; fn main() { let counter = Rc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Rc::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()); }
再一次編譯代碼,納尼, 居然又報了另一個錯誤, 成年人的崩潰誰能懂:
Rc<Mutex<i32>>` cannot be sent between threads safely`。這個錯誤編譯器告訴我們原因是:`the trait `Send` is not implemented for `Rc<Mutex<i32>>
。
Rc<T>
并不能安全的在線程間共享。當(dāng) Rc<T>
管理引用計數(shù)時,它必須在每一個 clone
調(diào)用時增加計數(shù),并在每一個克隆被丟棄時減少計數(shù)。Rc<T>
并沒有使用任何并發(fā)原語,來確保改變計數(shù)的操作不會被其他線程打斷。在計數(shù)出錯時可能會導(dǎo)致詭異的 bug,比如可能會造成內(nèi)存泄漏,或在使用結(jié)束之前就丟棄一個值。我們所需要的是一個完全類似 Rc<T>
,又以一種線程安全的方式改變引用計數(shù)的類型。
5.原子引用計數(shù)Arc<T>
在Rust標(biāo)準(zhǔn)庫中, 提供了一個名為Arc<T>的類型, 這是一個可以安全的用于并發(fā)環(huán)境的類型, 字母 “a” 代表 原子性(atomic),所以這是一個 原子引用計數(shù)(atomically reference counted)類型, 將代碼修改為:
use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; 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()); }
再次編譯代碼, 執(zhí)行結(jié)果如下:
這次終于得到結(jié)果10, 程序從0數(shù)到10, 雖然過程看上去并不明顯, 但我們卻學(xué)到了很多關(guān)于Mutex<T>和線程安全的內(nèi)容。
到此這篇關(guān)于Rust編程中的共享狀態(tài)并發(fā)執(zhí)行的文章就介紹到這了,更多相關(guān)Rust共享狀態(tài)并發(fā)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用Rust實現(xiàn)一個簡單的Ping應(yīng)用
這兩年Rust火的一塌糊涂,甚至都燒到了前端,再不學(xué)習(xí)怕是要落伍了。最近翻了翻文檔,寫了個簡單的Ping應(yīng)用練練手,感興趣的小伙伴可以了解一下2022-12-12