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