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