Rust 并發(fā)安全相關(guān)的幾個概念(上)
引言
本文介紹一下 Rust 并發(fā)安全相關(guān)的幾個概念:Send、Sync、Arc,Mutex、RwLock 等之間的聯(lián)系。這是其中的上篇,主要介紹 Send、Sync 這兩個trait。
Rust 的所有權(quán)概念
在展開介紹并發(fā)相關(guān)的幾個概念之前,有必要先了解一下 Rust 的所有權(quán)概念,Rust 對值(value)的所有權(quán)有明確的限制:
一個值只能有一個 owner。
可以同時存在同一個值的多個共享的非可變引用(immutable reference)。
但是只能存在一個值的可變引用(mutable reference)。
比如下面這段代碼,user 在創(chuàng)建線程之后,被移動(move)到兩個不同的線程中:
由于一個值只能有一個 owner,所以編譯器報錯,報錯信息如下:
Send 和 Sync 的約束作用
于是,如果一個類型會被多個線程所使用,是需要明確說明其共享屬性的。Send 和 Sync 這兩個 trait 作用就在于此,注意到這兩個 trait 都是 std::marker,實現(xiàn)這兩個 trait 并不需要對應(yīng)實現(xiàn)什么方法,可以理解為這兩個 trait 是類型的約束,編譯器通過這些約束在編譯時對類型進行檢查。到目前為止,暫時不展開對兩個概念的理解,先來看看兩者是如何在類型檢查中起約束作用的。比如 std::thread::spawn() 的定義如下:
可以看到,對于 spawn 傳入的函數(shù)和返回的類型,都要求滿足 Send 這個約束。結(jié)合前面 Send 的定義:
函數(shù)類型 F 需要滿足 Send 約束:這是因為創(chuàng)建線程之后,需要把函數(shù)類型傳入新創(chuàng)建的線程里,于是要求所有權(quán)能夠在線程之間傳遞。
返回類型需要滿足 Send 約束:這是因為創(chuàng)建線程之后,返回值也需要轉(zhuǎn)移回去原先的線程。
有了對類型的約束,編譯器就會在調(diào)用 std::thread::spawn 函數(shù)時針對類型進行檢查,比如下面這段代碼:
類型 Foo 標(biāo)記自己并不實現(xiàn) Send 這個 trait,于是在編譯的時候報錯了:
如果把 impl !Send for Foo {} 這一行去掉,代碼就能編譯通過了。
以上還有一個知識點:所有類型默認(rèn)都是滿足 Send、Sync 約束的,直到顯示聲明不滿足這個約束,比如上面的 impl !Send 就是這樣一個顯示聲明。這就帶來一個疑問:能不能跟編譯器耍一些心思,明明某個類型就不滿足這個約束,睜一只眼閉一只眼看看能不能在編譯器那里蒙混過關(guān)?
答案是不能,編譯器會檢查這個類型中所有包含的成員,只有所有成員都滿足這個約束,該類型才能算滿足約束??梢栽谏厦娴幕A(chǔ)上繼續(xù)做實驗,給 Foo 結(jié)構(gòu)體新增一個 Rc 類型的成員:
由于 Rc 并不滿足 Send 約束(即顯示聲明了impl !Send,見:impl-send1),導(dǎo)致類型 Foo 并不能蒙混過關(guān)滿足 Send 約束,編譯上面代碼時報錯信息如下:
因此:一個類型要滿足某個約束,當(dāng)且僅當(dāng)該類型下的所有成員都滿足該約束才行。理解 Send 和 Sync trait
理解 Send 和 Sync trait
繼續(xù)回到 Send 和 Sync 這兩個 trait 中來,兩者在 rust 官方文檔中定義如下:
Send:Types that can be transferred across thread boundaries。
?Sync:Types for which it is safe to share references between threads。
上面的定義翻譯過來:
Send 標(biāo)記表明該類型的所有權(quán)可以在線程之間傳遞。
Sync 標(biāo)記表明該類型的引用可以安全的在多個線程之間被共享。
我發(fā)現(xiàn)上面的這個解釋還是有點難理解了,可以換用更直白一點的方式來解釋這兩類約束:
Send:
滿足Send約束的類型,能在多線程之間安全的排它使用(Exclusive access is thread-safe)。
滿足Send約束的類型T,表示T和&mut T(mut表示能修改這個引用,甚至于刪除即drop這個數(shù)據(jù))這兩種類型的數(shù)據(jù)能在多個線程之間傳遞,說得直白些:能在多個線程之間move值以及修改引用到的值。
Sync:
滿足 Sync 約束的類型,能在多線程之間安全的共享使用(Shared access is thread-safe)。
滿足 Sync 約束的類型T,只表示該類型能在多個線程中讀共享,即:不能move,也不能修改,僅僅只能通過引用 &T 來讀取這個值。
有了上面的定義,可以知道:一個類型 T 的引用只有在滿足 Send 約束的條件下,類型 T 才能滿足 Sync 約束(a type T is Sync if and only if &T is Send)。即:T: Sync ≡ &T: Send。
對于那些基本的類型(primitive types)而言,比如 i32 類型,大多是同時滿足 Send 和 Sync 這兩個約束的,因為這些類型的共享引用(&)既能在多個多個線程中使用,同時也能在多個線程中被修改(&mut )。
了解了 Send 和 Sync 這兩類約束,就可以接著看在并發(fā)安全中的運用了,這是下一篇的內(nèi)容。
參考資料
Arc and Mutex in Rust | It's all about the bit2
Sync in std::marker - Rust3
Send in std::marker - Rust4
Send and Sync - The Rustonomicon5
rust - Understanding the Send trait - Stack Overflow6
Understanding Rust Thread Safety7
An unsafe tour of Rust’s Send and Sync | nyanpasu64’s blog8
Rust: A unique perspective9
關(guān)于 Databend
Databend 是一款開源、彈性、低成本,基于對象存儲也可以做實時分析的新式數(shù)倉。期待您的關(guān)注,一起探索云原生數(shù)倉解決方案,打造新一代開源 Data Cloud。
Databend 文檔:https://databend.rs/
Twitter:https://twitter.com/Datafuse_Labs
Slack:https://datafusecloud.slack.com/
Wechat:Databend
GitHub :https://github.com/datafuselabs/databend

Footnotes
https://doc.rust-lang.org/std/rc/struct.Rc.html#impl-Send??
https://itsallaboutthebit.com/arc-mutex??
https://doc.rust-lang.org/std/marker/trait.Sync.html??
https://doc.rust-lang.org/std/marker/trait.Send.html??
https://doc.rust-lang.org/nomicon/send-and-sync.html??
https://stackoverflow.com/questions/59428096/understanding-the-send-trait??
https://onesignal.com/blog/thread-safety-rust??
https://nyanpasu64.github.io/blog/an-unsafe-tour-of-rust-s-send-and-sync/#example-passing-mut-t-send-between-threads??
https://limpet.net/mbrubeck/2019/02/07/rust-a-unique-perspective.html??