多線程 | Rust學習筆記

做者:謝敬偉,江湖人稱「刀哥」,20年IT老兵,數據通訊網絡專家,電信網絡架構師,目前任Netwarps開發總監。刀哥在操做系統、網絡編程、高併發、高吞吐、高可用性等領域有多年的實踐經驗,並對網絡及編程等方面的新技術有濃厚的興趣。程序員

現代的CPU基本都是多核結構,爲了充分利用多核的能力,多線程都是繞不開的話題。不管是同步或是異步編程,與多線程相關的問題一直都是困難而且容易出錯的,本質上是由於多線程程序的複雜性,特別是競爭條件的錯誤,使得錯誤發生具有必定的隨機性,而隨着程序的規模愈來愈大,解決問題的難度也隨之愈來愈高。編程

其餘語言的作法

C/C++將同步互斥,以及線程通訊的問題所有交給了程序員。關鍵的共享資源通常須要經過Mutex/Semaphone/CondVariable之類的同步原語保證安全。簡單地說,就是須要加鎖。然而怎麼加,在哪兒加,怎麼釋放,都是程序員的自由。不加也能跑,絕大多數時候,也不會出問題。當程序的負載上來以後,不經意間程序崩潰了,而後就是痛苦地尋找問題的過程。安全

Go提供了經過channel的消息機制來規範化協程之間的通訊,可是對於共享資源,作法與C/C++沒有什麼不一樣。固然,遇到的問題也是相似。微信

Rust 作法

Go相似,Rust 也提出了channel機制用於線程之間的通訊。由於Rust 全部權的關係,沒法同時持有多個可變引用,所以channel被分紅了rxtx兩部分,使用起來沒有Go的那麼直觀和順手。事實上,channel的內部實現也是使用原子操做、同步原語對於共享資源的封裝。因此,問題的根源依然在於Rust如何操做共享資源。   Rust 經過全部權以及Type系統給出瞭解決問題的一個不一樣的思路,共享資源的同步與互斥再也不是程序員的選項,Rust代碼中同步及互斥相關的併發錯誤都是編譯時錯誤,強迫程序員在開發時就寫出正確的代碼,這樣遠遠好過面對在生產環境中頂着壓力排查問題的窘境。咱們來看一看這一切是如何作到的。網絡

Send,Sync 到底是什麼

Rust語言層面經過 std::marker 提供了 SendSync 兩個Trait。通常地說法,Send標記代表類型的全部權能夠在線程間傳遞,Sync標記代表一個實現了Sync 的類型能夠安全地在多個線程中擁有其值的引用。這段話很費解,爲了更好地理解SendSync,須要看一看這兩個約束到底是怎樣被使用的。如下是標準庫中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同時支持SendSync。很明顯Arc<RefCell<T>>不知足此條件,由於RefCell<T>不支持Sync。而Mutex<T>在其包裹的T支持Send的前提下,知足同時支持SendSync。實際上,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才能知足FutureSend約束條件。試圖轉移一個Rc<T>進入async block依然會被編譯器拒絕。如下代碼沒法經過編譯:

let a = Rc::new(100);
    let h = task::spawn(async move {
    	let b = a;
    });

此外,在異步代碼中,原則上應當避免使用同步的操做從而影響異步代碼的運行效率。試想一下,若是Future中調用了std::mutex::lock,則當前線程被掛起,Executor將再也不有機會執行其餘任務。爲此,異步運行庫通常提供了相似於標準庫的各類同步原語。這些同步原語不會掛起線程,而是當沒法獲取資源時返回Poll::PendingExecutor將當前任務掛起,執行其餘任務。

完美了麼?死鎖問題

Rust雖然用一種優雅的方式解決了多線程同步互斥的問題,但這並不能解決程序的邏輯錯誤。所以,多線程程序最使人頭痛的死鎖問題依然會存在於Rust的代碼中。因此說,所謂Rust「無懼併發」是有前提的。至少在目前,看不到編譯器能夠智能到分析並解決人類邏輯錯誤的水平。固然,屆時程序員這個崗位應該也就不存在了...


深圳星鏈網科科技有限公司(Netwarps),專一於互聯網安全存儲領域技術的研發與應用,是先進的安全存儲基礎設施提供商,主要產品有去中心化文件系統(DFS)、區塊鏈基礎平臺(SNC)、區塊鏈操做系統(BOS)。 微信公衆號:Netwarps

相關文章
相關標籤/搜索