Rust 中的無鎖編程技術
當多線程同時對共享資源讀寫的時候,我們需要用鎖去保護這個共享資源。
這裏有幾個概念,一條線程指的是進程中一個單一順序的控制流,一個進程中可以並行多個線程,每條線程並行執行不同的任務 [1],共享資源就是“堆(heap)” 中的一段數據,那麼們平常說的 “鎖” 究竟是什麼呢?而我們平常聽到的無鎖編程究竟是什麼呢?在 Rust 開源社區中,crossbeam 這個庫究竟給開發者們提供了什麼工具,解決了什麼問題呢?這些問題我們會慢慢在以後的文章中給大家解答。
我們要解決的核心問題
多線程帶來的最大問題在於其不確定性。在並行併發計算中,我們是沒有辦法控制 cpu 什麼時候執行哪條線程的,我們爲了最大限度地享受 cpu 所給我們帶來的性能優化,只需要將所有的工作交給 cpu,讓 cpu 自己決定什麼時候執行什麼指令。不僅僅是 cpu,實際上,編譯器,操作系統都會有自己的優化策略,都會給我們的程序運行帶來不確定性。所以我們在寫多線程程序的時候,腦海裏面時時刻刻要記住我們最大的三個敵人:
-
cpu
-
編譯器
-
操作系統
cpu 和編譯器都會根據自己的優化策略去更改指令的執行順序,而操作系統是負責線程調度的,什麼時候哪條線程獲得了 cpu 的使用權,這是我們無法控制的。
核心問題帶來的另一個問題
上面我們只提到了多線程編程指令執行順序上的不確定性,而忽略了另一個很重要的環節,那就是共享資源的內存管理問題。正是因爲線程執行順序上的不確定性,這使得對於共享內存的管理變得格外複雜。比如在處理無鎖數據結構的時候,從一個線程共享的鏈表裏面刪除一個節點,我們是否能立馬釋放其內存?答案是否定的,因爲我們不確定是否有別的線程還持有對這塊內存的引用。特別是對於 Rust 或者是 C 或者是 C++ 這樣的系統語言,這些語言沒有垃圾回收機制,要求編程者對於共享內存進行手動管理,至於如何管理,這也是我們以後會討論的問題。
鎖
鎖應該是最簡單也是最直接可以解決上述兩個痛點的解決方案了。鎖給我們對多線程的運行提供了一個非常好的 “模型”。這個模型是這樣的:
-
在堆上我們有一個共享數據。
-
同時有很多線程都在嘗試往這塊內存寫數據,或者是讀數據。
-
鎖就像一個門衛,這個門衛可以保證在任意的時間點,最多隻有一個線程對這塊內存有使用權,這個線程可以往裏面寫數據或者讀數據。
-
當共享的內存發現自身的引用量達到 0 的時候釋放內存。
那麼這個神奇的 “鎖” 究竟是什麼呢?在 Rust 中又是如何實現的呢?
鎖其實是一個協議(protocal),在 Rust 裏面我們稱之爲 trait。我們一起來看看這個 trait:
pub trait RawLock: Default + Send + Sync {
type Token: Clone;
fn lock(&self) -> Self::Token;
/// # Safety
///
/// `unlock()` should be called with the token given by the corresponding `lock()`.
unsafe fn unlock(&self, token: Self::Token);
}
這裏最主要要看到這幾個概念 Token lock 函數以及 unlock 函數的簽名。我們今天主要講講這個 token 是什麼。
token 我們可以將其理解爲一個證明,在編程語言的語義中,這個證明是證明當前持有這個 token 的線程可以對這個共享資源進行任何操作。而其他沒有這個證明的線程,只能卡在搶鎖的代碼處。
在 Rust 的 std 裏面,lock 函數返回的是一個 LockResult<MutexGuard<'_, T>>,暫時不用管 LockResult,MutextGuard 實際上就是對我們 token 的進一步的封裝。
那麼我們從以上的定義可以得到以下幾個信息:
-
任何的鎖,包括 spinlock,ticketlock,mcslock,等等,都必須實現以上的協議,也就是,必須提供 token,提供 lock 函數的實現,提供 unlock 函數的實現。
-
任何的鎖,都應該是 Send 以及 Sync 的,也就是說,任何的鎖都可以安全的在線程中轉移 (Send),並且其借用(&)也必須是可以安全的在線程中轉移 (Sync)。
-
lock 函數成功返回的話,代表當前線程獲得了一個可以訪問共享資源的證明。
-
unlock 函數的簽名中,需要提供 token 的 ownership,並且這個函數是 unsafe 的,雖然函數本身並沒有需要 deref raw pointer 這樣的操作,但語言本身是無法保證調用這個函數的線程傳入的 token 是否就是這個線程在之前調用 lock 成功時所返回的 token。所以我們加上 unsafe 來提醒使用者。
我們現在對 “鎖” 有一個初步的認識了,但是我們還沒有提及另一個非常重要的概念,就是 guard,也就是上面提及的對 token 的進一步的封裝,在下一篇文章中,我會再慢慢和大家解釋這個 “證明” 究竟是什麼。
[1] wiki: thread
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/chIEs7IYAYaw9G5OxWzk6g