如何理解 rust 中的 Sync Send?

2021-09-26 13:58:30 字數 3687 閱讀 2693

syncsend是 rust 安全併發中兩個至關重要的marker,但絕大多數的文件或書籍每當談到它們就只是直接丟擲它們的語義:

導火索 rwlock

我之所以決定徹底搞清楚這兩個東西是因為我使用標準庫中的rwlock遇到了一些問題,檢視原始碼之後發現這兩行(先不管send):

#[stable(feature = "rust1", since = "1.0.0")]

unsafe impl send for rwlock {}

#[stable(feature = "rust1", since = "1.0.0")]

unsafe impl sync for rwlock {}

稍懂 rust 的同學應該就可以看懂,這**的意思是,只有當型別t實現了syncrwlock才會實現sync

欸!?我就納悶了,讀寫鎖讀寫鎖,怎麼說也是個鎖。鎖不就是把不sync的型別變sync的存在嗎?

我馬上又去看了一下mutex,果然不出我所料:

#[stable(feature = "rust1", since = "1.0.0")]

unsafe impl send for mutex

#[stable(feature = "rust1", since = "1.0.0")]

unsafe impl sync for mutex

mutex看起來才像鎖,rwlock根本不符合我對鎖的印象。但我又仔細想想,互斥鎖和讀寫鎖到底差在哪兒,導致了這種情況呢?—— 讀寫鎖允許並行地讀。

所以答案很明了了,如果tsync,就不能讓多個執行緒同時拿到t型別物件的不可變引用

並行讀?記憶體不安全?

所以,並行唯讀會導致記憶體不安全嗎?這似乎不符合直覺。那到底是啥原因呢? 這裡可以思考一下,rust 的不可變引用真的「唯讀」嗎?當然不是了,大家耳熟能詳的cellrefcell就是拿不可變引用改變內部資料的典型用例。

所以,這個問題的本質是,rust 的不可變引用並沒有對內部可變性做過強的約束。當然我最初期望的是完全內部不可變的,而事實也如此,當你完全使用 「safe rust」 的時候。

比如像這樣:

struct a 

struct b

impl b

}

是 rust 型別系統不允許的,你永遠不能從不可變的b物件上安全地借到乙個可變的data引用。

當然, 你使用unsafe就可以達到目的。

struct a 

struct b

impl b ;

}}

在實際遇到這種場景時應該使用cell,refcell,rwlock等標準庫封裝好的結構(當然它們內部實現還得unsafe)。

所以只要 unsafe rust 還存在,你就不能假定不可變引用一定是「內部不可變」的。

我們為什麼需要使用 unsafe

這就是乙個歷史悠久的問題了,答案可以分很多方面,比如要ffi,再比如在某些場景使用 dirty 操作減少拷貝提公升效能。但回到我們的前文,我們為什麼要讓不可變引用「內部可變」呢?

我們先思考另乙個問題,如果我們不使用unsafe,在 rust 型別系統中,乙個物件的可變引用永遠只能同時存在乙個,這樣的話我們如果想在多個執行緒中使用可變引用要怎麼寫呢?

最後的結論就是我們不得不用,我們迫真地需要讓不可變引用「內部可變」的操作。那既然這個需求不可避免,我們又要怎樣保證 rust 的記憶體安全呢?

sync: make unsafe rust safe

我們再回到sync的定義:

所以,符合這個要求的型別有兩種:

第一種型別你永遠不能通過它的不可變引用改變它的內部,它所有的pub field都是sync的,然後所有的以&self作為receiverpub method也都不改變自身(或返回內部可變引用)。

第二種型別當然所有的pub field都得是sync的,但它可能存在以&self作為receiverpub method能改變自身(或返回內部可變引用),只不過這些方法本身得自己保證多執行緒訪問的安全性,比如使用鎖或者原子。

其它的型別都是!sync的。

我們可以舉幾個栗子看看:

所以,如果你自己寫了乙個並不天然實現sync的型別,不妨對照這兩條要求考慮要不要給它實現乙個sync

send: 安全共享

因為&mut tsend意味著move,而&tsend意味著share。要想多執行緒共享&tt就必須sync

包括像arc這樣的 "shared_pointer" 也是如此:

#[stable(feature = "rust1", since = "1.0.0")]

unsafe impl send for arc {}

當然,對於所有的非引用型別來說,大部分情況下並不需要開發者考慮它是否應該實現send,除非你要寫智慧型指標之類的東西,不然你只需要考慮給它實現乙個sync從而讓它的引用型別實現send

總結所以,我之所以覺得rwlock應該把!sync的型別包裝成sync的型別本質上是因為我錯誤地理解了sync的語義。rust 的可變引用要求過於嚴苛導致我們很多時候必須使用不可變引用來改變自身,所以sync是用來標記不可變借用可執行緒安全地訪問的。

至於可變引用,因為永遠只同時存在乙個可變引用,且其不與不可變引用共存,所以以可變引用為receiver的方法永遠是執行緒安全的,無需其它的約束。

對於rwlock, 你應該保證t是 sync 的,否則它就和refcell沒啥區別。只不過原本因為refcell會 panic 的邏輯現在會因為rwlock死鎖。

Rust中巨集的理解

巨集相比函式是相對難以理解的,更加難以掌握,編寫理解以及除錯都很有困難。但它的存在肯定是有它比較獨特的地方的。相比函式,巨集是用來生成 的,在呼叫巨集的地方,編譯器會先將巨集進行展開,生成 然後再編譯展開後的 在rust中,函式是不接受任意多個引數的,巨集可以辦到。巨集定義格式macro rules...

Rust模組的理解

特別注意,callerin這個mod必須在caller.rs中以pub mod callerin 形式宣告,否則外部看不到 最終模組路徑為 lip3 caller callerin call package name exp version 0.1.0 authors yujinliang 2857...

rust的綠卡 rust中的Pin詳解

rust中的pin詳解 相關概念 pin這是乙個struct,作用就是將p所指向的t在記憶體中固定住,不能移動。說白一些,就是不能通過safe 拿到 mut t。pin定義如下 pub struct pin pointer p,unpin 這是乙個trait,定義在std marker中,如果乙個t...