Windows 提供多種同步機制確保多執行緒資料一致性,例如 Mutex、Critical Section、SRW Lock 和根據地址的等待。Mutex 適用跨行程同步,但效能開銷較大;Critical Section 輕量但僅限單行程內使用;SRW Lock 更加輕量,適用讀寫分離場景;根據地址的等待則提供了更底層的同步控制。理解這些機制的特性,才能在不同場景選擇合適的工具,提升程式效能。Rust 則透過標準函式庫封裝這些機制,提供更安全、簡潔的併發程式設計模型。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Windows 同步機制深度解析與實作
Windows 作業系統提供了多種同步機制,以確保多執行緒環境下資料的一致性和正確性。這些機制包括重型同步物件、輕量級同步物件、Slim Reader-Writer 鎖以及根據地址的等待機制。本文將探討這些機制的工作原理、特點以及在 Rust 中的應用。
重型同步物件
Windows API 中的 Mutex 是典型的重型同步物件,用於在多執行緒環境中保護分享資源。Mutex 可以跨行程使用,但其使用成本較高,因為它涉及行程間的同步。
// 建立 Mutex 的範例
HANDLE hMutex = CreateMutex(NULL, FALSE, NULL);
if (hMutex == NULL) {
// 處理錯誤
}
// 使用 Mutex 進行同步
WaitForSingleObject(hMutex, INFINITE);
// 存取分享資源
ReleaseMutex(hMutex);
// 關閉 Mutex
CloseHandle(hMutex);
內容解密:
CreateMutex用於建立一個 Mutex 物件,第一個引數為安全屬性,第二個引數表示 Mutex 的初始狀態,第三個引數為 Mutex 名稱。WaitForSingleObject用於取得 Mutex,INFINITE表示無限等待直到 Mutex 可用。ReleaseMutex用於釋放 Mutex,使其他執行緒可以取得它。CloseHandle用於關閉 Mutex 控制程式碼,釋放資源。
輕量級同步物件:Critical Section
Critical Section 是 Windows 提供的一種輕量級同步機制,用於保護一段程式碼,使其不會被多個執行緒同時進入。它是一種遞迴 Mutex,但不允許跨行程使用。
// 初始化 Critical Section
CRITICAL_SECTION criticalSection;
InitializeCriticalSection(&criticalSection);
// 進入 Critical Section
EnterCriticalSection(&criticalSection);
// 存取分享資源
LeaveCriticalSection(&criticalSection);
// 銷毀 Critical Section
DeleteCriticalSection(&criticalSection);
內容解密:
InitializeCriticalSection初始化 Critical Section 物件。EnterCriticalSection用於進入 Critical Section,如果已經被其他執行緒佔用,則當前執行緒會被阻塞。LeaveCriticalSection用於離開 Critical Section,允許其他執行緒進入。DeleteCriticalSection用於銷毀 Critical Section,釋放資源。
Slim Reader-Writer 鎖 (SRW Lock)
從 Windows Vista 開始,Windows API 引入了 Slim Reader-Writer 鎖(SRW Lock),它是一種非常輕量級的鎖機制。SRW Lock 的大小隻有一個指標,可以靜態初始化,並且不需要手動銷毀。
// 初始化 SRW Lock
SRWLOCK srwLock = SRWLOCK_INIT;
// 取得獨佔鎖
AcquireSRWLockExclusive(&srwLock);
// 存取分享資源
ReleaseSRWLockExclusive(&srwLock);
// 取得分享鎖
AcquireSRWLockShared(&srwLock);
// 存取分享資源
ReleaseSRWLockShared(&srwLock);
內容解密:
SRWLOCK_INIT用於靜態初始化 SRW Lock。AcquireSRWLockExclusive用於取得獨佔鎖,ReleaseSRWLockExclusive用於釋放獨佔鎖。AcquireSRWLockShared用於取得分享鎖,ReleaseSRWLockShared用於釋放分享鎖。- SRW Lock 不會優先考慮讀取者或寫入者,嘗試按照先進先出的順序處理鎖請求。
根據地址的等待 (Address-Based Waiting)
Windows 8 引入了一種新的同步機制,稱為根據地址的等待,它與 Linux 的 FUTEX_WAIT 和 FUTEX_WAKE 操作類別似。這個機制允許執行緒在特定的原子變數上等待,並透過其他執行緒喚醒。
// 等待原子變數的值變為特定值
WaitOnAddress(&atomicVar, &expectedValue, sizeof(atomicVar), INFINITE);
// 喚醒等待執行緒
WakeByAddressSingle(&atomicVar);
內容解密:
WaitOnAddress用於在原子變數上等待,比較原子變數的值與預期值,如果相等則進入等待狀態。WakeByAddressSingle用於喚醒一個等待中的執行緒,WakeByAddressAll用於喚醒所有等待中的執行緒。- 這些函式操作的是原子變數的地址,因此可以實作高效的同步。
在 Rust 中的應用
Rust 的標準函式庫在 Windows 平台上使用了上述的一些同步機制。例如,std::sync::Mutex 在 Windows Vista 及之後的版本中直接包裝了 SRWLOCK,而 std::sync::Condvar 則使用了 CONDITION_VARIABLE。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
內容解密:
Arc(原子參照計數)用於在多個執行緒之間分享 Mutex。Mutex用於保護計數器的修改,確保多執行緒環境下資料的一致性。thread::spawn用於建立新的執行緒,每個執行緒都會對計數器進行遞增操作。join用於等待所有執行緒完成,最後列印最終的計數結果。
第9章:建立我們的鎖
在本章中,我們將建立自己的互斥鎖(Mutex)、條件變數(condition variable)和讀寫鎖(reader-writer lock)。對於每一個鎖,我們將從一個非常基本的版本開始,然後對其進行擴充套件,以提高其效率。
由於我們不會使用標準函式庫中的鎖型別(這樣做等於作弊),因此我們需要使用第8章中的工具,使執行緒能夠在不忙等待(busy-looping)的情況下等待。然而,正如我們在那一章中所看到的,不同平台提供的可用工具差異很大,這使得構建一個跨平台工作的鎖變得困難。
幸運的是,大多數現代作業系統都支援類別似futex的功能,或者至少支援喚醒和等待操作。正如我們在第8章中所看到的,Linux自2003年起就支援futex系統呼叫,Windows自2012年起支援WaitOnAddress函式家族,FreeBSD自2016年起支援_umtx_op系統呼叫,等等。
最值得注意的例外是macOS。儘管其核心支援這些操作,但它並沒有透過任何穩定、公開可用的C函式來暴露這些操作。然而,macOS確實附帶了一個最新版本的libc++,即C++標準函式庫的實作。這個函式庫包含了對C++20的支援,這是C++的一個版本,具有內建的非常基本的原子等待和喚醒操作(例如std::atomic
我們不會探討這些細節,而是使用crates.io上的atomic-wait crate為我們的鎖定原語提供構建塊。這個crate提供了三個函式:wait()、wake_one()和wake_all()。它為所有主要平台實作了這些函式,使用了我們上面討論的各種平台特定的實作。這意味著只要我們堅持使用這三個函式,就不必考慮任何平台特定的細節。
這些函式的行為與我們在第167頁的「Futex」中為Linux實作的同名函式類別似,但讓我們快速回顧一下它們的工作原理:
wait(&AtomicU32, u32)
這個函式用於等待,直到原子變數不再包含給定的值。如果原子變數中儲存的值等於給定值,則它會阻塞。當另一個執行緒修改了原子變數的值時,該執行緒需要在同一個原子變數上呼叫下面的喚醒函式之一,以喚醒等待中的執行緒。
這個函式可能會在沒有相應喚醒操作的情況下虛假傳回。因此,請務必在傳回後檢查原子變數的值,並在必要時重複呼叫wait()。
wake_one(&AtomicU32)
這會喚醒目前在同一個原子變數上阻塞在wait()上的單個執行緒。在修改原子變數後使用它,以通知一個等待中的執行緒有關更改。
wake_all(&AtomicU32)
這會喚醒目前在同一個原子變數上阻塞在wait()上的所有執行緒。在修改原子變數後使用它,以通知等待中的執行緒有關更改。
只有32位元的原子變數是被支援的,因為這是所有主要平台上唯一支援的大小。
在第167頁的「Futex」中,我們討論了一個非常簡單的例子,展示了這些函式在實踐中的使用方式。如果你已經忘記了,請務必在繼續之前檢視那個例子。
要使用atomic-wait crate,請在你的Cargo.toml檔案的[dependencies]部分新增atomic-wait = “1”;或者執行cargo add atomic-wait@1,這將為你完成這項工作。這三個函式定義在crate的根目錄中,可以使用use atomic_wait::{wait, wake_one, wake_all};匯入。
[dependencies]
atomic-wait = "1"
互斥鎖(Mutex)
我們將使用第4章中的SpinLock
讓我們從型別定義開始。與旋轉鎖相比,我們需要做一個更改:我們將使用一個設定為零或一的AtomicU32,而不是一個設定為false或true的AtomicBool,以便我們可以將其與原子等待和喚醒函式一起使用。
use std::sync::atomic::{AtomicU32, Ordering};
use std::cell::UnsafeCell;
pub struct Mutex<T> {
/// 0: 解鎖
/// 1: 鎖定
state: AtomicU32,
value: UnsafeCell<T>,
}
就像對於旋轉鎖一樣,我們需要承諾一個Mutex
unsafe impl<T> Sync for Mutex<T> where T: Send {}
我們還將新增一個MutexGuard型別,它實作了Deref特性,以提供一個完全安全的鎖定介面,就像我們在第80頁的「使用鎖定防護的安全介面」中所做的那樣:
use std::ops::{Deref, DerefMut};
pub struct MutexGuard<'a, T> {
mutex: &'a Mutex<T>,
}
impl<T> Deref for MutexGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.mutex.value.get() }
}
}
impl<T> DerefMut for MutexGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.mutex.value.get() }
}
}
Mutex操作實作
為了實作Mutex,我們需要實作lock和unlock操作。lock操作將檢查state變數是否為0(未鎖定),如果是,則將其設為1(鎖定)。如果state已經是1,則執行緒將等待直到它被解鎖。
impl<T> Mutex<T> {
pub fn new(value: T) -> Self {
Mutex {
state: AtomicU32::new(0),
value: UnsafeCell::new(value),
}
}
pub fn lock(&self) -> MutexGuard<T> {
loop {
if self.state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_ok() {
return MutexGuard { mutex: self };
}
wait(&self.state, 1);
}
}
}
impl<T> Drop for MutexGuard<'_, T> {
fn drop(&mut self) {
self.mutex.state.store(0, Ordering::Release);
wake_one(&self.mutex.state);
}
}
內容解密:
-
Mutex結構定義:Mutex包含一個
state欄位,用於表示鎖的狀態(0表示未鎖定,1表示鎖定),以及一個value欄位,用於儲存實際的資料。 -
MutexGuard實作:MutexGuard是一個防護型別,它在建立時鎖定Mutex,在銷毀時自動解鎖Mutex。這確保了即使在發生panic時,鎖也會被釋放。
-
lock操作:lock函式嘗試將
state從0更改為1。如果成功,則傳回一個MutexGuard。如果state已經是1,則執行緒將等待直到它被解鎖。 -
unlock操作:當MutexGuard被丟棄時,它會將
state設回0並喚醒一個等待中的執行緒(如果有的話)。
Mutex 操作流程
graph LR
B[B]
A[開始] --> B{state == 0?}
B -->|是| C[鎖定state]
B -->|否| D[等待]
C --> E[傳回MutexGuard]
D --> B
F[MutexGuard銷毀] --> G[解鎖state]
G --> H[喚醒等待執行緒]
圖表翻譯:
此圖表展示了Mutex的操作流程。開始時,執行緒會檢查state是否為0。如果是,則鎖定state並傳回MutexGuard。如果state已經是1,則執行緒將等待直到它被解鎖。當MutexGuard被銷毀時,它會解鎖state並喚醒任何等待中的執行緒。