作者:謝敬偉,江湖人稱“刀哥”,20年IT老兵,數(shù)據(jù)通信網(wǎng)絡(luò)專家,電信網(wǎng)絡(luò)架構(gòu)師,目前任Netwarps開(kāi)發(fā)總監(jiān)。刀哥在操作系統(tǒng)、網(wǎng)絡(luò)編程、高并發(fā)、高吞吐、高可用性等領(lǐng)域有多年的實(shí)踐經(jīng)驗(yàn),并對(duì)網(wǎng)絡(luò)及編程等方面的新技術(shù)有濃厚的興趣。
現(xiàn)代的CPU基本都是多核結(jié)構(gòu),為了充分利用多核的能力,多線程都是繞不開(kāi)的話題。無(wú)論是同步或是異步編程,與多線程相關(guān)的問(wèn)題一直都是困難并且容易出錯(cuò)的,本質(zhì)上是因?yàn)槎嗑€程程序的復(fù)雜性,特別是競(jìng)爭(zhēng)條件的錯(cuò)誤,使得錯(cuò)誤發(fā)生具備一定的隨機(jī)性,而隨著程序的規(guī)模越來(lái)越大,解決問(wèn)題的難度也隨之越來(lái)越高。
其他語(yǔ)言的做法
C/C++將同步互斥,以及線程通信的問(wèn)題全部交給了程序員。關(guān)鍵的共享資源一般需要通過(guò)Mutex/Semaphone/CondVariable之類的同步原語(yǔ)保證安全。簡(jiǎn)單地說(shuō),就是需要加鎖。然而怎么加,在哪兒加,怎么釋放,都是程序員的自由。不加也能跑,絕大多數(shù)時(shí)候,也不會(huì)出問(wèn)題。當(dāng)程序的負(fù)載上來(lái)之后,不經(jīng)意間程序崩潰了,然后就是痛苦地尋找問(wèn)題的過(guò)程。
Go提供了通過(guò)channel的消息機(jī)制來(lái)規(guī)范化協(xié)程之間的通信,但是對(duì)于共享資源,做法與C/C++沒(méi)有什么不同。當(dāng)然,遇到的問(wèn)題也是類似。
Rust 做法
與Go類似,Rust 也提出了channel機(jī)制用于線程之間的通信。因?yàn)?code>Rust 所有權(quán)的關(guān)系,無(wú)法同時(shí)持有多個(gè)可變引用,因此channel被分成了rx和tx兩部分,使用起來(lái)沒(méi)有Go的那么直觀和順手。事實(shí)上,channel的內(nèi)部實(shí)現(xiàn)也是使用原子操作、同步原語(yǔ)對(duì)于共享資源的封裝。所以,問(wèn)題的根源依然在于Rust如何操作共享資源。
?
Rust 通過(guò)所有權(quán)以及Type系統(tǒng)給出了解決問(wèn)題的一個(gè)不同的思路,共享資源的同步與互斥不再是程序員的選項(xiàng),Rust代碼中同步及互斥相關(guān)的并發(fā)錯(cuò)誤都是編譯時(shí)錯(cuò)誤,強(qiáng)迫程序員在開(kāi)發(fā)時(shí)就寫(xiě)出正確的代碼,這樣遠(yuǎn)遠(yuǎn)好過(guò)面對(duì)在生產(chǎn)環(huán)境中頂著壓力排查問(wèn)題的窘境。我們來(lái)看一看這一切是如何做到的。
Send,Sync 究竟是什么
Rust語(yǔ)言層面通過(guò) std::marker 提供了 Send 和 Sync 兩個(gè)Trait。一般地說(shuō)法,Send標(biāo)記表明類型的所有權(quán)可以在線程間傳遞,Sync標(biāo)記表明一個(gè)實(shí)現(xiàn)了Sync 的類型可以安全地在多個(gè)線程中擁有其值的引用。這段話很費(fèi)解,為了更好地理解Send 和 Sync,需要看一看這兩個(gè)約束究竟是怎樣被使用的。以下是標(biāo)準(zhǔn)庫(kù)中std::thread::spawn()的實(shí)現(xiàn):
pub fn spawn<F, T>(self, f: F) -> io::Result<JoinHandle<T>>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
{
unsafe { self.spawn_unchecked(f) }
}
可以看到,創(chuàng)建一個(gè)線程,需要提供一個(gè)閉包,而這個(gè)閉包的約束是 Send ,也就是需要能轉(zhuǎn)移到線程中,閉包返回值T的約束也是 Send(這個(gè)不難理解,線程運(yùn)行后返回值需要轉(zhuǎn)移回去) 。舉例說(shuō)明,以下代碼無(wú)法通過(guò)編譯。
let a = Rc::new(100);
let h = thread::spawn(move|| {
let b = *a+1;
});
h.join();
編譯器指出,std::rc::Rc<i32> cannot be sent between threads safely。原因在于,閉包的實(shí)現(xiàn)在內(nèi)部是由編譯器創(chuàng)建一個(gè)匿名結(jié)構(gòu),將捕獲的變量存入此結(jié)構(gòu)。以上代碼閉包大致被翻譯成:
struct {
a: Rc::new(100),
...
}
而Rc<T>是不支持 Send 的數(shù)據(jù)類型,因此該匿名結(jié)構(gòu),即這個(gè)閉包,也不支持 Send ,無(wú)法滿足std::thread::spawn()關(guān)于F的約束。
上面代碼改用Arc<T>,則編譯通過(guò),因?yàn)?code>Arc<T>是一種支持 Send的數(shù)據(jù)類型。但是Arc<T>不允許共享可變引用,如果想實(shí)現(xiàn)多線程之間修改共享資源,則需要使用Mutex<T>來(lái)包裹數(shù)據(jù)。代碼會(huì)改為這個(gè)樣子:
let mut a = Arc::new(Mutex::new(100));
let h = thread::spawn(move|| {
let mut shared = a.lock().unwrap();
*shared = 101;
});
h.join();
為什么Mutex<T>可以做到這一點(diǎn),能否改用RefCell<T>完成相同功能?答案是否定的。我們來(lái)看一下這幾個(gè)數(shù)據(jù)類型的限定:
unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}
unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {}
unsafe impl<T: ?Sized> Send for RefCell<T> where T: Send {}
impl<T: ?Sized> !Sync for RefCell<T> {}
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
Arc<T>可以Send,當(dāng)其包裹的T同時(shí)支持Send和Sync。很明顯Arc<RefCell<T>>不滿足此條件,因?yàn)?code>RefCell<T>不支持Sync。而Mutex<T>在其包裹的T支持Send的前提下,滿足同時(shí)支持Send和Sync。實(shí)際上,Mutex<T>的作用就是將一個(gè)支持Send的普通數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)化為支持Sync,進(jìn)而可以通過(guò)Arc<T>傳入線程中。我們知道,多線程下訪問(wèn)共享資源需要加鎖,所以Mutex::lock()正是這樣一個(gè)操作,lock()之后便獲取到內(nèi)部數(shù)據(jù)的可變引用。
?
通過(guò)上述分析,我們看到Rust另辟蹊徑,利用所有權(quán)以及Type系統(tǒng)在編譯時(shí)刻解決了多線程共享資源的問(wèn)題,的確是一個(gè)巧妙的設(shè)計(jì)。
異步代碼,協(xié)程
異步代碼同步互斥問(wèn)題與同步多線程代碼沒(méi)有本質(zhì)不同。異步運(yùn)行庫(kù)一般提供類似于std::thread::spawn()的方式來(lái)創(chuàng)建協(xié)程/任務(wù),以下是async-std創(chuàng)建一個(gè)協(xié)程/任務(wù)的API:
pub fn spawn<F, T>(future: F) -> JoinHandle<T>
where
F: Future<Output = T> + Send + 'static,
T: Send + 'static,
{
Builder::new().spawn(future).expect("cannot spawn task")
}
可以看到,與std::thread::spawn()非常相似,閉包換成了Future,而Future要求Send約束。這意味著參數(shù)future必須可以Send。我們知道,async語(yǔ)法通過(guò)generaror生成了一個(gè)狀態(tài)機(jī)驅(qū)動(dòng)的Future,而generaror與閉包類似,捕獲變量,放入一個(gè)匿名數(shù)據(jù)結(jié)構(gòu)。所以這里變量必須也是Send才能滿足Future的Send約束條件。試圖轉(zhuǎn)移一個(gè)Rc<T>進(jìn)入async block依然會(huì)被編譯器拒絕。以下代碼無(wú)法通過(guò)編譯:
let a = Rc::new(100);
let h = task::spawn(async move {
let b = a;
});
此外,在異步代碼中,原則上應(yīng)當(dāng)避免使用同步的操作從而影響異步代碼的運(yùn)行效率。試想一下,如果Future中調(diào)用了std::mutex::lock,則當(dāng)前線程被掛起,Executor將不再有機(jī)會(huì)執(zhí)行其他任務(wù)。為此,異步運(yùn)行庫(kù)一般提供了類似于標(biāo)準(zhǔn)庫(kù)的各種同步原語(yǔ)。這些同步原語(yǔ)不會(huì)掛起線程,而是當(dāng)無(wú)法獲取資源時(shí)返回Poll::Pending,Executor將當(dāng)前任務(wù)掛起,執(zhí)行其他任務(wù)。
完美了么?死鎖問(wèn)題
Rust雖然用一種優(yōu)雅的方式解決了多線程同步互斥的問(wèn)題,但這并不能解決程序的邏輯錯(cuò)誤。因此,多線程程序最令人頭痛的死鎖問(wèn)題依然會(huì)存在于Rust的代碼中。所以說(shuō),所謂Rust“無(wú)懼并發(fā)”是有前提的。至少在目前,看不到編譯器可以智能到分析并解決人類邏輯錯(cuò)誤的水平。當(dāng)然,屆時(shí)程序員這個(gè)崗位應(yīng)該也就不存在了...
深圳星鏈網(wǎng)科科技有限公司(Netwarps),專注于互聯(lián)網(wǎng)安全存儲(chǔ)領(lǐng)域技術(shù)的研發(fā)與應(yīng)用,是先進(jìn)的安全存儲(chǔ)基礎(chǔ)設(shè)施提供商,主要產(chǎn)品有去中心化文件系統(tǒng)(DFS)、區(qū)塊鏈基礎(chǔ)平臺(tái)(SNC)、區(qū)塊鏈操作系統(tǒng)(BOS)。
微信公眾號(hào):Netwarps