再談 Send 與 Sync | Rust學習筆記

Send 與 Sync 多是Rust多線程以及異步代碼種最多見到的約束。在前面一篇討論多線程的文章中介紹過這兩個約束的由來。可是,真正書寫比較複雜的代碼時,仍是會常常遇到編譯器的各類不配合。這裏借用個人同事遇到的一個問題再次舉例談一談 Send 與 Sync 的故事。程序員

基本場景

C/C++中不存在Send/Sync的概念,數據對象能夠任意在多線程中訪問,只不過須要程序員保證線程安全,也就是所謂「加鎖」。而在Rust中,因爲全部權的設計,不能直接將一個對象分紅兩份或多份,每一個線程都放一份。通常地,若是一份數據僅僅子線程使用,咱們會將數據的值轉移至線程中,這也是Send的基礎含義。所以,Rust代碼常常會看到將數據clone(),而後move到線程中:編程

let b = aa.clone();
thread::spawn(move || {
    b...            
})

假如,數據須要在多線程共享,狀況會複雜一些。咱們通常不會在線程中直接使用外部環境變量引用。緣由很簡單,生命週期的問題。線程的閉包要求‘static,這會與被借用的外部環境變量的生命週期衝突,錯誤代碼以下:安全

let bb = AA::new(8);
thread::spawn( || {
    let cc = &bb;  //closure may outlive the current function, but it borrows `bb`, which is owned by the current function
});

包裹一個Arc能夠解決這個問題,Arc剛好就是用來管理生命週期的,改進後的代碼以下:微信

let b = Arc::new(aa);
let b1 = b.clone();
thread::spawn(move || {
    b1...
})

Arc提供了共享不可變引用的功能,也就是說,數據是隻讀的。若是咱們須要訪問多線程訪問共享數據的可變引用,即讀寫數據,那麼還須要在原始數據上先包裹Mutex<T>,相似於RefCell<T>,提供內部可變性,所以咱們能夠獲取內部數據的&mut,修改數據。固然,這須要經過Mutex::lock() 來操做。網絡

let b = Arc::new(Mutex::new(aa));
let b1 = b.clone();
thread::spawn(move || {
    let b = b1.lock();
    ...
})

爲何不能直接使用RefCell完成這個功能?這是由於RefCell不支持 Sync,沒辦法裝入Arc。注意Arc的約束:多線程

unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}

Arc<T>是Send,條件是 T:Send+Sync。RefCell不知足 Sync,所以 Arc<RefCell<>> 不知足Send,沒法轉移至線程中。錯誤代碼以下:閉包

let b = Arc::new(RefCell::new(aa));
let b1 = b.clone();
thread::spawn(move || {
	^^^^^^^^^^^^^ `std::cell::RefCell<AA<T>>` cannot be shared between threads safely
    let x = b1.borrow_mut();
    
})

異步代碼:跨越 await 問題

如上所述,通常地,咱們會將數據的值轉移入線程,這樣只須要作正確的 Send和Sync 標記便可,很直觀,容易理解。典型的代碼以下:架構

fn test1<T: Send + Sync + 'static>(t: T) {
    let b = Arc::new(t);
    let bb = b.clone();
    thread::spawn( move|| {
        let cc = &bb;
    });
}

根據上面的分析,不難推導出條件 T: Send + Sync + 'static 的前因後果:Closure: Send + 'static ⇒ Arc<T>: Send + ’static ⇒ T: Send + Sync + 'static。併發

然而,在異步協程代碼中有一種常見狀況,推導過程則顯得比較隱蔽,值得說道說道。考察如下代碼:異步

struct AA<T>(T);

impl<T> AA<T> {
    async fn run_self(self) {}
    async fn run(&self) {}
    async fn run_mut(&mut self) {}
}

fn test2<T: Send + 'static>(mut aa: AA<T>) {
    let ha = async_std::task::spawn(async move {
        aa.run_self().await;
    });
}

test2 中,限定 T: Send + ‘static,合情合理。async fn 生成的 GenFuture 要求 Send + ‘static,所以被捕獲置入 GenFuture 匿名結構中的 AA 也必須知足 Send + ‘static,進而要求AA 泛型參數也知足Send + ‘static。

然而,相似的方式調用 AA::run() 方法,編譯失敗,編譯器提示 GenFuture 不知足 Send。代碼以下:

fn test2<T: Send + 'static>(mut aa: AA<T>) {
    let ha = async_std::task::spawn(async move {
    	     ^^^^^^^^^^^^^^^^^^^^^^ future returned by `test2` is not `Send`
        aa.run().await;
    });
}

緣由在於,AA::run()方法的簽名是 &self,因此run()是經過 aa 的不可變借用 &AA 來調用。而run()又是一個異步方法,執行了await,也就是所謂的&aa 跨越了 await,故而要求GenFuture匿名結構除了生成aa以外,還須要生成 &aa,示意代碼以下:

struct {
    aa: AA
    aa_ref: &AA	
}

正如以前探討過,生成的 GenFuture須要知足 Send,所以 AA 以及 &AA 都須要知足 Send。而&AA知足 Send,則意味着 AA 知足 Sync。這也就是各類 Rust教程中都會提到的那句話的真正含義:

對於任意類型 T,若是 &T是 Send ,T 就是 Sync 的

以前出錯的代碼修改成以下形式,增長 Sync標記,編譯經過。

fn test2<T: Send + Sync + 'static>(mut aa: AA<T>) {
    let ha = async_std::task::spawn(async move {
          aa.run().await;
    });
}

另外,值得指出的是上述代碼中調用 AA::run_mut(&mut self) 不須要 Sync 標記:

fn test2<T: Send + 'static>(mut aa: AA<T>) {
    let ha = async_std::task::spawn(async move {
          aa.run_mut().await;
    });
}

這是由於 &mut self 並不要求 T: Sync。參見如下標準庫中關於Sync定義代碼就明白了:

mod impls {
    #[stable(feature = "rust1", since = "1.0.0")]
    unsafe impl<T: Sync + ?Sized> Send for &T {}
    #[stable(feature = "rust1", since = "1.0.0")]
    unsafe impl<T: Send + ?Sized> Send for &mut T {}
}

能夠看到,&T: Send 要求 T: Sync,而 &mut T 則 T: Send 便可。

總結

總而言之,Send約束在根源上是由 thread::spawn() 或是 task::spawn() 引入的,由於兩個方法的閉包參數必須知足 Send。此外,在須要共享數據時使用Arc<T>會要求 T: Send + Sync。而共享可寫數據,須要Arc<Mutex<T>>,此時 T: Send 便可,再也不要求Sync。

異步代碼中關於 Send/Sync 與同步多線程代碼沒有不一樣。只是由於GenFuture 的特別之處使得跨越 await 的變量必須是 T: Send,此時須要注意經過 T 調用異步方法的簽名,若是爲 &self,則必須知足 T:Send + Sync。

最後,一點經驗分享:關於 Send/Sync 的道理並不複雜,更多時候是由於代碼中層次比較深,調用關係複雜,致使編譯器的錯誤提示很難看懂,某些特定場合編譯器可能還會給出徹底錯誤的修正建議,這時候須要仔細斟酌,追根溯源,找到問題的本質,不能徹底依靠編譯器提示。


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


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

相關文章
相關標籤/搜索