Try   HackMD

Crust of Rust : Send, Sync, and their implementors

直播錄影

  • 主機資訊
    ​​​​wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx ~/CrustOfRust> neofetch --stdout
    ​​​​wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx 
    ​​​​----------------------------------------------- 
    ​​​​OS: Ubuntu 22.04.3 LTS x86_64 
    ​​​​Host: HP Pavilion Plus Laptop 14-eh0xxx 
    ​​​​Kernel: 6.2.0-37-generic 
    ​​​​Uptime: 22 mins 
    ​​​​Packages: 2367 (dpkg), 11 (snap) 
    ​​​​Shell: bash 5.1.16 
    ​​​​Resolution: 2880x1800 
    ​​​​DE: GNOME 42.9 
    ​​​​WM: Mutter 
    ​​​​WM Theme: Adwaita 
    ​​​​Theme: Yaru-dark [GTK2/3] 
    ​​​​Icons: Yaru [GTK2/3] 
    ​​​​Terminal: gnome-terminal 
    ​​​​CPU: 12th Gen Intel i5-12500H (16) @ 4.500GHz 
    ​​​​GPU: Intel Alder Lake-P 
    ​​​​Memory: 8412MiB / 15695MiB 
    
  • Rust 編譯器版本 :
    ​​​​wilson@wilson-HP-Pavilion-Plus-Laptop-14-eh0xxx ~/CrustOfRust> rustc --version
    ​​​​rustc 1.70.0 (90c541806 2023-05-31) (built from a source tarball)
    

Why Send and Sync?

0:00:00

This time we go over the Send and Sync marker traits from the standard library, as well as some of the most important implementors (and non-implementors) of them. You can find the nomicon entry for Send/Sync here: https://doc.rust-lang.org/nomicon/send-and-sync.html.

What are Send and Sync?

0:01:31

Rust 的 Send 和 Sync 用來描述型別層級的 thread-safety。Java 則是用 collections 來檢測你是否有在走訪元素的時候修改它,還有其他語言是在執行時期去檢測是否可能存在有問題的操作。

在許多情況下,實際上只是文件告訴你給定值或給定型別是否可以跨執行緒邊界使用或由多個執行緒同時使用。在 Rust 中,這些概念都被融入到型別系統中,因此至少在大多數情況下,你可以透過型別檢查來檢查程式的 thread-safety 和正確性。

Marker traits

0:02:51

SendSync 的共同點是,它們皆為 marker trait。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
Module std::marker
Primitive traits and types representing basic properties of types.

marker trait 的命名由來是,所有 marker traits 都是沒有方法的,它們僅用於標記該型別滿足給定性質,或具有特定性質或有關它的保證,但它不會賦予任何額外的行為。例如,如果某個東西實作了 Send trait,那麼你就不會在其上獲得某種 send 方法,它只是告訴你該型別是 Send。

Auto traits

0:03:50

Send 被標記為 auto trait :

pub unsafe auto trait Send { // empty. }

這並不直接與它是一個 marker trait 這一事實相結合,並非所有 marker trait 都是 auto trait,但它是一個 auto trait 這一事實意味著如果所有型別的成員本身都具有相同的 trait,編譯器將自動為你實作這個 trait。你可以想像,擁有一個不是 marker trait 的 auto trait 會很奇怪,因為 auto trait 是為所有成員都實作該 trait 的任何事物 implicitly 實作的,但如果你必須在那裡實作一個方法,則不清楚你將如何將其委派給內部型別。所以一般來說,所有的 auto trait 都是 marker trait,但並非所有的 marker trait 都是 auto trait

The Send trait

0:05:45

Send 告訴你某個型別的值可以安全地傳遞給另一個執行緒。具體來說,就是將此值的所有權傳遞給另一個執行緒。從那一刻起,該執行緒可以對該值進行任何操作,可以將其視為不僅僅是該執行緒可以讀取或寫入該值,而是該執行緒可以對該值進行任意操作。大多數型別通常都是 Send,一個原始型別 (bool, i32, etc.) ,將其傳遞給另一個執行緒是沒有問題的,因為它只是一個原始型別。

Types that aren't Send

0:06:49

!Send 的型別主要是指那些如果將它們傳遞給另一個執行緒,那麼該執行緒可能會違反底層型別的某些 implicit 假設或 invariant。其中有兩個主要的例子。一個是 Rc,我們將會討論為什麼這是一個問題。另一個經常出現的例子是 MutexGuard。這既適用於標準的 Mutex,也適用於 RwLock。這並不是所有 Mutex 的普遍特性,而是特別是標準函式庫中由作業系統實作的那些 Mutex 通常是 !Send,因為至少在某些作業系統上,獲得 lock 的執行緒必須是釋放 lock 的同一個執行緒。如果你在執行緒 A 上 lock,則不允許執行緒 B 嘗試釋放執行緒 A 獲得的 lock,必須由執行緒 A 來釋放該 lock,請注意,這並不意味著 Mutex/RwLock 型別本身是 !Send,具體來說,!Send 的是 guard。

舉一個更一般屬性的例子,如果你有型別的 Drop 實作,並且參考了作為物件建立的一部分而建立的 thread local state,那麼這通常意味著該型別不能是 Send,因為想像一下,你在執行緒 A 上建立這個物件,它使用 A 上的 thread local state。然後你將它發送到執行緒 B,然後執行緒 B 嘗試卸除它 (超出 scope 之類的原因),然後,當執行緒 B 卸除它時,如果該卸除嘗試存取相同 thread local state,它將嘗試存取 B 的 thread local state,而不是 A 的 thread local state,即使 A 是它用來建立物件的內容。因此,你最終會違反一些內部 invaraint,而這通常不會得到好結果。

Digging into Rc

0:09:40

開始建置 Rust 專案 :

$ cargo new sharing-is-caring
$ cd sharing-is-caring
$ vim src/main.rs

先實作一個簡易版 Rc 來說明 Rc 有什麼問題 (詳細版 Rc 自行參考 : Rc) :

struct Rc<T> { inner: *mut Inner<T>, } struct Inner<T> { count: usize, value: T, } impl<T> Rc<T> { pub fn new(v: T) -> Self { Rc { inner: Box::into_raw(Box::new(Inner { count: 1, value: v, })), } } } impl<T> Clone for Rc<T> { fn clone(&self) -> Self { unsafe { &mut *self.inner }.count += 1; // 1. 這裡的 unsafe 之所以不會有問題是因為, // Rc 是 !Send (不允許離開目前執行緒) 以及 !Sync, // 也就不會有其他執行緒並行地存取 inner。 // 2. count 不需要是 AtomicUsize, // 因為我們知道只有一個執行緒會去操作 Rc。 // // 1, 2 的說明在本程式的其他 unsafe 地方同理 // // 1, 2 的說明都基於 Rc 為 !Send Rc { inner: self.inner, } } } impl<T> Drop for Rc<T> { fn drop(&mut self) { let cnt = &mut unsafe { &mut *self.inner }.count; if *cnt == 1 { let _ = unsafe { Box::from_raw(self.inner) }; // self.inner 是 raw pointer, // 需要先轉成 Box 再卸除才能連同 heap 裡的資料也卸除。 } else { *cnt -= 1; } } } impl<T> std::ops::Deref for Rc<T> { type Target = T; fn deref(&self) -> &Self::Target { &unsafe { &*self.inner }.value } } fn main() { let x = Rc::new(1); let y = x.clone(); // std::thread::spawn 的函式簽章 : // pub fn spawn<F, T>(f: F) -> JoinHandle<T> // where // F: FnOnce() -> T + Send + 'static, // T: Send + 'static, std::thread::spawn(move || { let _ = y; }); }

預期無法編譯成功 (Line 3 的 *mut 不應該為 Send,且 std::thread::spawn 函式要求型別必須為 Send),卻編譯成功 :

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/sharing-is-caring`

找出根本原因為何剛剛的程式可以編譯,先寫個函式來測試 :

fn foo<T: Send>(_: T) {} fn bar(x: Rc<()>) { foo(x); }
目前程式碼
struct Rc<T> { inner: *mut Inner<T>, } struct Inner<T> { count: usize, value: T, } impl<T> Rc<T> { pub fn new(v: T) -> Self { Rc { inner: Box::into_raw(Box::new(Inner { count: 1, value: v, })), } } } impl<T> Clone for Rc<T> { fn clone(&self) -> Self { unsafe { &mut *self.inner }.count += 1; Rc { inner: self.inner, } } } impl<T> Drop for Rc<T> { fn drop(&mut self) { let cnt = &mut unsafe { &mut *self.inner }.count; if *cnt == 1 { let _ = unsafe { Box::from_raw(self.inner) }; } else { *cnt -= 1; } } } impl<T> std::ops::Deref for Rc<T> { type Target = T; fn deref(&self) -> &Self::Target { &unsafe { &*self.inner }.value } } fn foo<T: Send>(_: T) {} fn bar(x: Rc<()>) { foo(x); } fn main() { let x = Rc::new(1); let y = x.clone(); std::thread::spawn(move || { let _ = y; }); }

編譯器確實認為 *mut 是 !Send :

$ cargo run
   Compiling sharing-is-caring v0.1.0 (C:\Users\user\wilson\CrustOfRust\sharing-is-caring)
error[E0277]: `*mut Inner<()>` cannot be sent between threads safely
  --> src\main.rs:62:9
   |
62 |     foo(x);
   |     --- ^ `*mut Inner<()>` cannot be sent between threads safely
   |     |
   |     required by a bound introduced by this call
...

std::thread::spawn 使用到 y,猜測是否是編譯器很聰明的關係 (編譯器或許認為 let _ = y; 不需要將 y 移動到新的執行緒,所以不需要 Send):

fn main() { let x = Rc::new(1); let y = x.clone(); std::thread::spawn(move || { let _ = y; + drop(y); }); }
目前程式碼
struct Rc<T> { inner: *mut Inner<T>, } struct Inner<T> { count: usize, value: T, } impl<T> Rc<T> { pub fn new(v: T) -> Self { Rc { inner: Box::into_raw(Box::new(Inner { count: 1, value: v, })), } } } impl<T> Clone for Rc<T> { fn clone(&self) -> Self { unsafe { &mut *self.inner }.count += 1; Rc { inner: self.inner, } } } impl<T> Drop for Rc<T> { fn drop(&mut self) { let cnt = &mut unsafe { &mut *self.inner }.count; if *cnt == 1 { let _ = unsafe { Box::from_raw(self.inner) }; } else { *cnt -= 1; } } } impl<T> std::ops::Deref for Rc<T> { type Target = T; fn deref(&self) -> &Self::Target { &unsafe { &*self.inner }.value } } fn main() { let x = Rc::new(1); let y = x.clone(); std::thread::spawn(move || { let _ = y; drop(y); }); }

成功出現編譯錯誤 :

$ cargo run ... error[E0277]: `*mut Inner<i32>` cannot be sent between threads safely --> src\main.rs:62:24 | 62 | std::thread::spawn(move || { | ------------------ ^------ | | | | _____|__________________within this `[closure@src\main.rs:62:24: 62:31]` | | | | | required by a bound introduced by this call 63 | | let _ = y; 64 | | drop(y); 65 | | }); | |_____^ `*mut Inner<i32>` cannot be sent between threads safely

繼續探討假設 Rc 為 Send 會發生什麼問題 :

... impl<T> Drop for Rc<T> { fn drop(&mut self) { let cnt = &mut unsafe { &mut *self.inner }.count; if *cnt == 1 { let _ = unsafe { Box::from_raw(self.inner) }; } else { *cnt -= 1; } } } ... fn main() { let x = Rc::new(1); let y = x.clone(); std::thread::spawn(move || { drop(y); }); drop(x); }

執行到 Line 18 結束時,參考計數為 2,接著往下執行時,有兩種情況發生

  • memory leak
    如果兩個執行緒想要並行地卸除值,兩個執行緒都看到參考計數為 2,接著兩個執行緒都會執行到 Line 10,並將參考計數減為 0,此時參考計數為 0,卻沒有執行到 Line 8,而導致 memory leak。
  • multiple-free
    如果有多個執行緒並行地卸除值時,都看到 cnt 值為 1,因此執行了 Line 8 多次,最終導致某個記憶體被釋放多次。

編譯器並不對執行緒有任何特別的了解。它只知道這個 std::thread::spawn 函式不會起作用,因為它需要被傳遞的型別實作 Send。因此,編譯器會檢查,傳入的 closure 中的所有內容是否都實作了 Send。Rc 沒有實作,因此編譯器會提示錯誤訊息。

The Sync trait

0:23:52

Sync 和 Send 之間有相當密切的關係。事實上,它們之間的關係是如此密切,以至於 Sync 幾乎是根據 Send 定義的。具體來說,如果一個 &T 是 Send,那麼型別 T 就是 Sync。如果你有一個型別,該型別的參考允許在不同的執行緒之間共享,那麼該型別就是 Sync。即使該型別本身不能被傳遞給其他執行緒。

例如,我不能給你一個 MutexGuard,但如果我有一個 MutexGuard,我可以給你一個對該 MutexGuard 的參考。這是因為,而這個區別之所以重要,是因為我們不能允許其他執行緒卸除它,但如果我們只是給了一個對已經存在的 MutexGuard 的共享參考,則其他執行緒不會替我們卸除它,其他執行緒不會獲得任何擁有權,也無法執行任何對該 MutexGuard 的修改操作,因為這需要 mutable 參考 (&mut T),不可以僅是共享參考 (&T),所以其他執行緒只能從中讀取。這一切都是可以接受的,沒有任何操作會導致它們被卸除,這正是我們關心的事情。

然而,Rc 不能是 Sync,它是同樣的問題。如果 Rc 是 Sync,那麼我可以將一個 Rc 的參考給另一個執行緒,然後那個執行緒只需對其呼叫 clone。我們知道,clone 實作也要求所有存取都在同一個執行緒上進行。這就是為什麼 Rc 是 !Send 和 !Sync,而 MutexGuard 是 !Send,但它是 Sync 的原因。

Send + !Sync

0:26:21

Send + !Sync 最主要的例子有 interior mutablility 性質的型別,例如 Cell 以及 RefCell。Cell 不會給出內部型別的參考,以下為 Cell 的操作 :

pub const fn new(value: T) -> Cell<T> pub fn set(&self, val: T) pub fn into_inner(self) -> T // 耗用 self ...

沒有一個函式回傳內部型別的參考。這樣做沒問題的原因是,想像一下你在單獨的執行緒上,如果值被封裝在 Cell 內並且你從未給出它的參考,那麼在任何給定的時間點,你知道你對內部值有 exclusive 存取權。因為這不會提供參考給其他人,也沒有其他執行緒正在存取該值,因此,這樣做是可以的。再次回歸到 Cell 是 !Sync 的事實,沒有其他執行緒擁有對此型別的共享參考,你也沒有給出對內部值的任何共享參考,因此,在執行時,你是唯一能夠存取到 Cell 內部的東西的人。因此,即便你只有一個共享參考,你也可以安全地修改 Cell 內部的值,但是這都基於 Cell 是 !Sync 的事實。

Cell 確實是 Send,因為你沒有提供任何參考,因此,如果你將 Cell 提供給另一個執行緒,本執行緒中不會留下任何可能干擾該執行緒修改值的東西。如果我有一個 Cell,並把它給你,我不可能擁有任何對 Cell 的參考,因為那樣我就不被允許移動它,或者對 Cell 內部的東西進行存取,因為一開始就沒有提供共享參考的存取器。因此,將其發送是完全沒問題的。一旦我發送它,另一個執行緒就可以自由地進行修改內部值之類的操作,因為它可以依賴於相同的 invariant,即沒有其他執行緒擁有對 Cell 的共享參考,並且在這個執行緒上,對 Cell 的任何共享參考都沒有在當前執行,因為只涉及一個執行緒。


Q : shouldnt cell not demand !sync on T then?
A : 先看到 Cell<T> 實作的 Send :

§ impl<T> Send for Cell<T> where T: Send + ?Sized,

T 有 Send bound,因為如果你將 Cell<T> 發送到別的執行緒時,會連同 Cell 內部的 T 也一起發送。

再看到 Cell<T> 實作的 !Sync :

impl<T> !Sync for Cell<T> where T: ?Sized,

T 並沒有 !Sync bound。這告訴我們,無論內部型別是什麼,Cell 永遠不會是 !Sync。

所以有一個問題,即使你不將 Cell 發送到其他執行緒,Cell 是否應該要求內部型別是 Sync? 答案是你並不需要這個要求,因為如果我將一個 Cell 提供給另一個執行緒,那麼就不涉及任何參考,我正在提供實際的內部值,所以我所需要的只是內部值是 Send 即可。


Q : Is it good to use Cell directly though? I mostly see code wrapping it inside some Box or Rc
A : 單獨的 Cell 不是非常有用,因為如果你可以擁有一個自己的 T 並且只取參考,那你為什麼要使用 Cell 呢?Cell 之所以有用是因為想像你在單執行緒上進行類似樹走訪的操作,而該樹可能有循環的情況導致一個節點可能拜訪多次,所以節點的存取只能以共享存取的方式,不能以 exclusive 存取的方式,但因為你是單執行緒的,你知道即使只有共享存取,也可以在原地對其進行修改,這就是你可能希望能夠通過共享參考修改值的情況。有時你會看到它透過 Rc 實作,原因大致相同,因為你可能有一些資料結構,其中你可能希望將給定值儲存在多個位置,但如果你曾經存取過它,你確實希望能夠對其進行修改。標準函式庫就有這個實作的例子 : RcCell

再討論一下 Cell 的 Copy bound :

impl<T> Cell<T> where T: Copy, // Cell 要求 T 的型別是 Copy : pub fn get(&self) -> T // 之所以要求 T 是 Copy 是因為, // 當你在呼叫 get 時,直接給出 Copy 的值即可

Negative implementations

0:32:41

Negative 實作為 unstable feature。Unstable feature 叫做 negative impl,它允許你實作以下 :

impl !Sync for Args {}

通常 Rust 程式不能這樣寫,因為 negative 實作是一個 unstable feature。然而,你可以為型別實作 Send,告訴編譯器,「即使你因為 auto-trait 的緣故而認為這個型別是 !Send,但我告訴你,這個型別實際上是 Send」,這樣做是不安全的,因為如果編譯器知道你結構的某個內部型別是 !Send 或是 !Sync。它假設這意味著你的型別也是 !Send 和 !Sync。但你可以告訴它,我自己已經檢查過了。但為了這樣做,這是不安全的,因為你宣稱了編譯器無法保證為真的事情。因此,對 Send 的 positive 實作是不安全的,negative 實作是不穩定的,但它並不是不安全的。

為什麼我們會想要為一個型別實作 !Send 呢?這通常是相當罕見的情況。通常情況下,編譯器的自動推斷會過度 negative 而不是過度 positve。一般來說,你的型別不會在你希望它們不實作 Send 時實作它們,而是在你不希望它們實作 Send 時實作它們。Negative 實作通常會出現在你寫的程式碼中,其中內部只包含了 Send 型別,例如它們包含了 Box 或整數等,但你正在進行一些操作,例如存取執行緒區域變數,而存取該執行緒區域變數的行為使其不是 thread safe,但內部型別並不具有 unsafe 的特性。例如,MutexGuard 可能就是這樣的一個例子,其中包含的內容可能是完全可以 Send 的,但由於這種執行緒區域變數的存取,這使得它是 !Send,而這是編譯器無法為你推斷出來的。

Sending mutable references

0:35:17

看到 Send 的 implementors :

// 幾乎是 Sync 的定義 impl<'_, T> Send for &'_ T where T: Sync + ?Sized, impl<'_, T> Send for &'_ mut T where T: Send + ?Sized,

為何 exclusive 參考的 T 也需要是 Send ? 因為我沒有放棄實際的 T,我只是放棄了對它的參考,即使它是一個 mutable 參考。當然,這背後的原因可以看到 std::mem::replace 這樣的方法 :

pub fn replace<T>(dest: &mut T, src: T) -> T // Moves src into the referenced dest, // returning the previous dest value.

這裡的問題是,replace 意味著你可以擁有你只是獲得了 mutable 參考的東西的所有權。所以如果我給你一個 T 的 mutable 參考,如果那個 T 是 !Send,你可以使用這個方法來取 T,然後卸除它。再想像一下一個 MutexGuard。如果我給你一個 MutexGuard 的 mutable 參考,你可以拿到自己的 lock,交換兩個 MutexGuard ,然後卸除我的 MutexGuard,這是不可以的。這就是為什麼對於 mutable 參考,T 需要有 Send bound 的原因。

Raw pointers

0:37:00

為什麼需要 explicit Send 實作? 為什麼沒有 auto-trait 實作在 Send 的列表中列出 ? 因為列表還有其他 !Send 的 implementors :

impl<T> !Send for *const T where T: ?Sized, source impl<T> !Send for *mut T where T: ?Sized, ...

這些 !Send 存在的原因並不是它們必須存在,如果你有一個原始指標,沒有什麼能阻止你發送該指標或將同步應用於該指標,因為最終它還是 unsafe 的去解參考。那麼為什麼要這些實作呢?它們明明根本上不是 !Send 或 !Sync 的。Negative 實作之所以存在,完全是為了讓如果有人編寫一個型別時對於 Send 和 Sync 的規則不是非常清楚時,我們會以安全的方式失敗,也就是說,如果它們的型別中含有原始指標,它們的型別會被認為是 !Send 和 !Sync,因為很可能他們沒有考慮過 thread-safety。

我們寧願這些型別是 !Send 和 !Sync,而不是它們在不適當的情況下意外地可發送和可同步。這可以看作是對 auto-trait 的一種對策,也就是說,如果你的型別包含原始指標,那麼你很可能必須仔細考慮 thread-safety。因此,我們將要求你為那些型別放入手動實作的 Send 和 Sync。因此,如果你將原始指標放入其中,我們將故意打破 auto-trait 推斷

std::sync::mpsc and !Sync

0:41:54

!Sync 在標準函式庫的例子 :

impl<T> !Sync for Receiver<T>

Placement of T: Send/Sync bounds

0:42:30

先看到 channel :

// Struct std::sync::mpsc::Sender impl<T> Sender<T> pub fn send(&self, t: T) -> Result<(), SendError<T>> // T 沒有 Send bound // Trait std::marker::Send impl<T: Send> Send for Sender<T> // 只有 T 本身是 Send 的情況下,才會為 Sender<T> 實作 Send

T 之所以不用是 Send 的原因是,如果你建立 channel 的 Sender-Receiver pair,但從未將它們移動到其他執行緒,那麼你發送的 T 本身並不需要是 Send,因為它們從未跨越執行緒邊界。所以你會看到這在 Sender 函式庫中是一個相當常見的模式,只有在 Sender 被發送時才需要 T 是 Send。

因此,只有在 T 是 Send 的情況下,才能將 Sender 和 Receiver 移動到不同的執行緒。因此,你可以建立一個 !Send T 的 channel,在同一個執行緒中使用它是沒問題的。只有在嘗試將 channel 的任一端移動到執行緒邊界時,它才要求內部的 T 是 Send。

繼續看到 Arc :

impl<T> Arc<T> source pub fn new(data: T) -> Arc<T> // T 沒有 Send 以及 Sync bound // 對於 Arc 來說,Sync 更加相關,因為一般情況下, // 你無法從 Arc<T> 獲取 mutable 參考,但這是可能的,如 get_mut。 pub fn get_mut(this: &mut Arc<T, A>) -> Option<&mut T> // get_mut 只有在計數為 1 的時候才會拿到 mutable 參考 impl<T, A> Send for Arc<T, A> where T: Sync + Send + ?Sized, A: Allocator + Send, // 只有 T 本身是 Send + Sync 的情況下, // 才會為 Arc<T, A>實作 Send

只有在 T 是 Send + Sync 的情況下,才能將 Arc 移動到不同的執行緒。要求是 Sync 是因為別的執行緒可能有 Arc 的參考,而要求 Send 的原因是無論哪個執行緒卸除最後一個 Arc,都會卸除內部型別,而你無法控制是哪個執行緒。因此,T 必須是 Send,因為卸除它的執行緒將擁有它的所有權。而這可能是與創建 T 的執行緒不同的執行緒。

Q : if it is a refrence then it need to be 'static. or you have to move it by "move"
A : 不知道你在問什麼。

Q : moving, sending a ref requires Sync, which means it can be accessed by different threads
A : 是的,這就是我們剛提到的 :

impl<'_, T> Send for &'_ T where T: Sync + ?Sized,

Per-OS impl Send for guards

0:46:04

Q : Can we explicitly implement Send for Guards in the certain OS?
A : 這是一個很好的問題。實際上,你可能不太想這麼做,因為現在,一個型別在某些方面是否為 thread safe 取決於你所在的作業系統,這會讓除錯變得非常奇怪。這樣做確實有它的吸引力,但最好還是統一標準。另外,要求 MutexGuard 為 !Send 可能還能讓你在它上面進行一些更多的最佳化。因此,儘管在某些作業系統上,也許在不同的執行緒上卸除 Mutex 是可以接受的,但你可能還是可以基於它不會被傳遞進行一些最佳化。

more std::sync::mpsc and !Sync

0:47:06

新的標準函式庫已移除 Sender 的 !Sync 實作。

Is Send/Sync auto-implemented?

0:48:40

Q : Is Send trait default implemented on users own structs?
A : 這回歸到了它是一個 auto-trait 的事實。因此,結構是否實作 Send 取決於它包含的型別。如果結構的所有內部型別本身都是 Send,那麼結構也將是 Send。如果你的內部型別包含了 Rc、MutexGuard、或者一個 &T 且其中的 T 不是 Sync,或者任何種類的原始指標,那麼你的型別將不是 Send。

The nomicon Send/Sync entry

0:50:40

Send and Sync

Major exceptions include:

  • raw pointers are neither Send nor Sync (because they have no safety guards).
  • UnsafeCell isn't Sync (and therefore Cell and RefCell aren't).
    UnsafeCellCellRefCell 是 !Sync 的更深層原因。這是因為 UnsafeCell 是 Rust 中唯一允許透過共享參考進行 mutable 存取的基本型別,這就是 CellRefCell 內部使用的方式,因為從根本上說,它們是在只給出共享參考的情況下進行修改的。因此,它們必須通過 UnsafeCell 進行操作。
    UnsafeCell 本身是 !Sync,因為如果你若使用這種型別,Rust 的開發者希望強迫你仔細考慮你的型別是否應該是 Sync,鑒於你內部使用了這種不安全的構造。這就是為什麼 Cell 和 RefCell 透過它們的 auto-trait 不實作 Sync 的原因。
  • Rc isn't Send or Sync (because the refcount is shared and unsynchronized).

There's no magic!

0:52:57

對於 Send 和 Sync,並不存在除了它們是 auto-trait 之外的編譯器魔法。就編譯器而言,它並不了解 thread-safety。編譯器只知道某些型別,例如某些原始型別實作了 Send,某些原始型別實作了 Sync。如果一個型別的所有內部型別都實作了 Send,那麼該型別也實作了 Send,Sync 也是如此。這就是 auto-trait 的作用。然後,編譯器知道有一些函式有一個 bound,要求一個型別是 Send 或 Sync。它對這些進行型別檢查的方式與對任何其他特性進行檢查的方式相同。因此,Send 和 Sync 完全沒有被內建到編譯器中。它們唯一依賴的是 auto-trait。

關於 Send 和 Sync,你應該知道的一件事是,由於它們是 marker trait 且有些特殊,你可以將它們作為額外的 trait 包含在 dynamical trait dispatch 中 :

fn foo(_: Box<dyn Iterator<Item = ()>>) {} // dyn 只能有一個 trait,若有額外的 trait 則不合法 : // fn foo(_: Box<dyn Iterator<Item = () + Clone>>) {} // 想要一個 trait 物件包含多個 trait 的暫時作法是,先組合再使用 : trait IteratorAndDebug: Iterator<Item = ()> + std::fmt::Debug {} fn foo(_: Box<dyn IteratorAndDebug>) {} // 若額外的 trait 是 Send/ Sync,則是可以允許的 fn foo(_: Box<dyn Iterator<Item = ()> + Send + Sync >) {}

+ Send 很常在 Async/ Await 看到,例如類似以下函式 :

fn foo(_: Box<dyn std::future::Future<Output = ()> + Send>) {}

Future 是不是 Sync 並不重要的原因是,為了 poll Future,本來就需要 mutable 參考。而 Future 要是 Send 是因為通常有多執行緒的 Executors,想像一個 Future 正在這個執行緒上執行,但由於網路 socket 尚未準備好或其他原因而暫停。因此,執行緒池中的某個其他執行緒會選擇執行其他 Future,然後,當這個原本的 Future 再次變得可用時,某個其他執行緒可能正在 idle,它可能會選擇執行該 Future。但這意味著 Future 現在移至了不同的執行緒。因此,我們需要確保該 Future 是 Send 以確保該操作有效。

因此,通常你會看到像 tokio 這樣的多執行緒 Executor 要求傳遞給它的 Future 是 Send,除非你使用一種特定的構造允許你執行 !Sync 的 Future。在 tokio 中,這種構造被稱為 LocalSet,它允許你 spawn Future,但這些 Future 不允許離開當前 Executor 執行緒。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
0:57:40
Q : Are there other basic methods other than thread::spawn that require Send or Sync?
A : Arc, channel,但最終這兩者是否是 Send 和 Sync 只關係到 thread::spawn 需要。對於 tokio 來說也是一樣的,它要求 Send 和 Sync 是因為在深層的某個地方會 spawn 新的執行緒,而這些執行緒,它們的參數需要被發送,這些參數,在某種意義上是 channel 的 Receiver,它們必須接收任務,因此這些 Sender 必須是 Send,這意味著 Sender 內部的型別必須是 Send,這就是為什麼 Future 必須是 Send 的原因。在最底層,你只是試圖在不同的執行緒上執行一些東西,這些東西必須是 Send,這意味著它所接受的任何輸入和對任何其他東西的處理都必須是 Send,這就是整個過程的樞紐所在。

所以問題是,這個 graph 是否有其他的 root,而不僅僅是 thread::spawn 嗎?實際上沒有,想像如果你直接自己實作 thread::spawn,透過類似 libc calls,或者說極端的版本是類似 fork 這樣的東西,但如果你要自己實作這種基本的東西,你需要寫 Send 和 Sync 的 bound,但實際上,對於大多數 Rust 程式碼來說,這歸結為深層呼叫 graph 中的某處有一個 thread::spawn,並不一定是你直接呼叫 thread::spawn,它可能是有一個 Sender,但 Receiver 被發送到了一個 thread::spawn,因此,為了獲取該 Sender,當成對物件被創建時,Receiver 去了那個執行緒,但 Sender 卻到了你這裡,這意味著 Sender 必須為 Send。

Q : Doesn't thread::spawn require object being moved to be both Sync+Send ?
A : thread::spawn 只需要該型別是 static 的並且是 Send。它不需要 Sybc,因為它不會參考任何東西。需要它是 static 的原因是因為沒有任何保證該執行緒不會超出當前執行緒的壽命。如果我在我的 stack 上配置了一些值,然後我創建一個執行緒並且它有一個對我的 stack 的參考,那麼該執行緒可能在我回傳後繼續執行,然而該參考將不再有效。標準函式庫中現在有一些工作正在添加 thread::scope
這允許你使用不需要 static 參數的執行緒,這些工作的原理基本上是確保當前執行緒或當前的 stack frame 在執行緒 join 之前不會回傳。因此,你知道該執行緒不會超出當前 stack frame 的壽命,因此不需要 static。thread::scope 是一個相當巧妙的添加。很久以前,它曾經存在於標準函式庫中,然後發現存在問題,因此被移除了,但現在它又回來了,而且在 crossbeam crate 中也存在了一段時間。

Q : Could a type be sync but not send? Does that ever make sense?
A : MutexGuard

Negative impls on stable

1:02:46

Q : How to implement a !Sync+Send ?
A : 看到以下例子 :

// 假設 T 實作 Send + Sync,MutexGuard 會自動實作 Send + Sync struct MutexGuard<'a, T> { i: &'a mut T, } // 標準的編譯器目前不能這麼做 impl<T> !Send for MutexGuard<'_, T> {} // 但你可以加一個成員讓 MutexGuard 變成 Send + !Sync struct MutexGuard<'a, T> { i: &'a mut T, _not_send: std::marker::PhantomData<std::rc::Rc<()>>, // PhantomData 表示你不用配置儲存 Rc 的記憶體空間, // 它只是用來告訴編譯器要怎麼做而已。 } // 另一種作法是用到 unsafe 讓 MutexGuard 變成 Send + !Sync unsafe impl<T> Send for MutexGuard<'_, T> {} // 同理,unsafe 也可以讓 MutexGuard 變成 !Send + Sync unsafe impl<T> Sync for MutexGuard<'_, T> {} // 實作 Sync 和 Send 都是 unsafe 的特性, // 因為編譯器的 auto-traits 推斷說這些型別不是 Send 和 Sync, // 然後編譯器會說,這些型別不應該實作這些特性, // 而你卻聲稱它們應該是 Send 和 Sync,這是 unsafe 的, // 你聲稱了編譯器認為不是 thread-safe 的型別是 thread-safe 的。 // 這通常是 unsafe 的含義,unsafe 的含義是編譯器跟你說 : // 「如果你這麼聰明,你就證明它」,這就是編譯器的真正做法。 // 因此,手動實作這些是 unsafe 的。

Q : How are the auto trait parts implemented? Compiler magic?
A : auto-trait 是編譯器的魔法。當編譯器建構一個型別時,會查看所有包含的型別,如果所有內部型別都符合標記為 auto-trait,那麼外部型別也將實作該 trait。

待整理

  1. Negative implementations 整個小節
  2. 0:57:40