做者:謝敬偉,江湖人稱「刀哥」,20年IT老兵,數據通訊網絡專家,電信網絡架構師,目前任Netwarps開發總監。刀哥在操做系統、網絡編程、高併發、高吞吐、高可用性等領域有多年的實踐經驗,並對網絡及編程等方面的新技術有濃厚的興趣。程序員
現代的CPU
基本都是多核結構,爲了充分利用多核的能力,多線程都是繞不開的話題。不管是同步或是異步編程,與多線程相關的問題一直都是困難而且容易出錯的,本質上是由於多線程程序的複雜性,特別是競爭條件的錯誤,使得錯誤發生具有必定的隨機性,而隨着程序的規模愈來愈大,解決問題的難度也隨之愈來愈高。編程
其餘語言的作法
C/C++
將同步互斥,以及線程通訊的問題所有交給了程序員。關鍵的共享資源通常須要經過Mutex/Semaphone/CondVariable之類的同步原語保證安全。簡單地說,就是須要加鎖。然而怎麼加,在哪兒加,怎麼釋放,都是程序員的自由。不加也能跑,絕大多數時候,也不會出問題。當程序的負載上來以後,不經意間程序崩潰了,而後就是痛苦地尋找問題的過程。安全
Go
提供了經過channel
的消息機制來規範化協程之間的通訊,可是對於共享資源,作法與C/C++
沒有什麼不一樣。固然,遇到的問題也是相似。微信
Rust 作法
與Go
相似,Rust
也提出了channel
機制用於線程之間的通訊。由於Rust
全部權的關係,沒法同時持有多個可變引用,所以channel
被分紅了rx
和tx
兩部分,使用起來沒有Go
的那麼直觀和順手。事實上,channel
的內部實現也是使用原子操做、同步原語對於共享資源的封裝。因此,問題的根源依然在於Rust
如何操做共享資源。 Rust
經過全部權以及Type
系統給出瞭解決問題的一個不一樣的思路,共享資源的同步與互斥再也不是程序員的選項,Rust
代碼中同步及互斥相關的併發錯誤都是編譯時錯誤,強迫程序員在開發時就寫出正確的代碼,這樣遠遠好過面對在生產環境中頂着壓力排查問題的窘境。咱們來看一看這一切是如何作到的。網絡
Send,Sync 到底是什麼
Rust
語言層面經過 std::marker
提供了 Send
和 Sync
兩個Trait
。通常地說法,Send
標記代表類型的全部權能夠在線程間傳遞,Sync
標記代表一個實現了Sync
的類型能夠安全地在多個線程中擁有其值的引用。這段話很費解,爲了更好地理解Send
和 Sync
,須要看一看這兩個約束到底是怎樣被使用的。如下是標準庫中std::thread::spawn()
的實現:數據結構
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) } }
能夠看到,建立一個線程,須要提供一個閉包,而這個閉包的約束是 Send
,也就是須要能轉移到線程中,閉包返回值T
的約束也是 Send
(這個不難理解,線程運行後返回值須要轉移回去) 。舉例說明,如下代碼沒法經過編譯。多線程
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。緣由在於,閉包的實如今內部是由編譯器建立一個匿名結構,將捕獲的變量存入此結構。以上代碼閉包大體被翻譯成:閉包
struct { a: Rc::new(100), ... }
而Rc<T>
是不支持 Send
的數據類型,所以該匿名結構,即這個閉包,也不支持 Send
,沒法知足std::thread::spawn()
關於F
的約束。架構
上面代碼改用Arc<T>
,則編譯經過,由於Arc<T>
是一種支持 Send
的數據類型。可是Arc<T>
不容許共享可變引用,若是想實現多線程之間修改共享資源,則須要使用Mutex<T>
來包裹數據。代碼會改成這個樣子:併發
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>
能夠作到這一點,可否改用RefCell<T>
完成相同功能?答案是否認的。咱們來看一下這幾個數據類型的限定:
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
,當其包裹的T
同時支持Send
和Sync
。很明顯Arc<RefCell<T>>
不知足此條件,由於RefCell<T>
不支持Sync
。而Mutex<T>
在其包裹的T
支持Send
的前提下,知足同時支持Send
和Sync
。實際上,Mutex<T>
的做用就是將一個支持Send
的普通數據結構轉化爲支持Sync
,進而能夠經過Arc<T>
傳入線程中。咱們知道,多線程下訪問共享資源須要加鎖,因此Mutex::lock()
正是這樣一個操做,lock()
以後便獲取到內部數據的可變引用。 經過上述分析,咱們看到Rust
另闢蹊徑,利用全部權以及Type
系統在編譯時刻解決了多線程共享資源的問題,的確是一個巧妙的設計。
異步代碼,協程
異步代碼同步互斥問題與同步多線程代碼沒有本質不一樣。異步運行庫通常提供相似於std::thread::spawn()
的方式來建立協程/任務,如下是async-std
建立一個協程/任務的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
約束。這意味着參數future
必須能夠Send
。咱們知道,async
語法經過generaror
生成了一個狀態機驅動的Future
,而generaror
與閉包相似,捕獲變量,放入一個匿名數據結構。因此這裏變量必須也是Send
才能知足Future
的Send
約束條件。試圖轉移一個Rc<T>
進入async block
依然會被編譯器拒絕。如下代碼沒法經過編譯:
let a = Rc::new(100); let h = task::spawn(async move { let b = a; });
此外,在異步代碼中,原則上應當避免使用同步的操做從而影響異步代碼的運行效率。試想一下,若是Future
中調用了std::mutex::lock
,則當前線程被掛起,Executor
將再也不有機會執行其餘任務。爲此,異步運行庫通常提供了相似於標準庫的各類同步原語。這些同步原語不會掛起線程,而是當沒法獲取資源時返回Poll::Pending
,Executor
將當前任務掛起,執行其餘任務。
完美了麼?死鎖問題
Rust
雖然用一種優雅的方式解決了多線程同步互斥的問題,但這並不能解決程序的邏輯錯誤。所以,多線程程序最使人頭痛的死鎖問題依然會存在於Rust
的代碼中。因此說,所謂Rust
「無懼併發」是有前提的。至少在目前,看不到編譯器能夠智能到分析並解決人類邏輯錯誤的水平。固然,屆時程序員這個崗位應該也就不存在了...
深圳星鏈網科科技有限公司(Netwarps),專一於互聯網安全存儲領域技術的研發與應用,是先進的安全存儲基礎設施提供商,主要產品有去中心化文件系統(DFS)、區塊鏈基礎平臺(SNC)、區塊鏈操做系統(BOS)。 微信公衆號:Netwarps