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);

內容解密:

  1. CreateMutex 用於建立一個 Mutex 物件,第一個引數為安全屬性,第二個引數表示 Mutex 的初始狀態,第三個引數為 Mutex 名稱。
  2. WaitForSingleObject 用於取得 Mutex,INFINITE 表示無限等待直到 Mutex 可用。
  3. ReleaseMutex 用於釋放 Mutex,使其他執行緒可以取得它。
  4. 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);

內容解密:

  1. InitializeCriticalSection 初始化 Critical Section 物件。
  2. EnterCriticalSection 用於進入 Critical Section,如果已經被其他執行緒佔用,則當前執行緒會被阻塞。
  3. LeaveCriticalSection 用於離開 Critical Section,允許其他執行緒進入。
  4. 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);

內容解密:

  1. SRWLOCK_INIT 用於靜態初始化 SRW Lock。
  2. AcquireSRWLockExclusive 用於取得獨佔鎖,ReleaseSRWLockExclusive 用於釋放獨佔鎖。
  3. AcquireSRWLockShared 用於取得分享鎖,ReleaseSRWLockShared 用於釋放分享鎖。
  4. SRW Lock 不會優先考慮讀取者或寫入者,嘗試按照先進先出的順序處理鎖請求。

根據地址的等待 (Address-Based Waiting)

Windows 8 引入了一種新的同步機制,稱為根據地址的等待,它與 Linux 的 FUTEX_WAIT 和 FUTEX_WAKE 操作類別似。這個機制允許執行緒在特定的原子變數上等待,並透過其他執行緒喚醒。

// 等待原子變數的值變為特定值
WaitOnAddress(&atomicVar, &expectedValue, sizeof(atomicVar), INFINITE);

// 喚醒等待執行緒
WakeByAddressSingle(&atomicVar);

內容解密:

  1. WaitOnAddress 用於在原子變數上等待,比較原子變數的值與預期值,如果相等則進入等待狀態。
  2. WakeByAddressSingle 用於喚醒一個等待中的執行緒,WakeByAddressAll 用於喚醒所有等待中的執行緒。
  3. 這些函式操作的是原子變數的地址,因此可以實作高效的同步。

在 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());
}

內容解密:

  1. Arc(原子參照計數)用於在多個執行緒之間分享 Mutex。
  2. Mutex 用於保護計數器的修改,確保多執行緒環境下資料的一致性。
  3. thread::spawn 用於建立新的執行緒,每個執行緒都會對計數器進行遞增操作。
  4. 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::wait())。雖然由於各種原因,從Rust中使用這些功能有些棘手,但這是完全可能的,這為我們在macOS上提供了基本的類別似futex的等待和喚醒功能。

我們不會探討這些細節,而是使用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型別作為構建我們的Mutex的參考。在不涉及阻塞的部分,例如防護(guard)型別的設計,將保持不變。

讓我們從型別定義開始。與旋轉鎖相比,我們需要做一個更改:我們將使用一個設定為零或一的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可以在執行緒之間共用,即使它包含一個可怕的UnsafeCell:

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);
    }
}

內容解密:

  1. Mutex結構定義:Mutex包含一個state欄位,用於表示鎖的狀態(0表示未鎖定,1表示鎖定),以及一個value欄位,用於儲存實際的資料。

  2. MutexGuard實作:MutexGuard是一個防護型別,它在建立時鎖定Mutex,在銷毀時自動解鎖Mutex。這確保了即使在發生panic時,鎖也會被釋放。

  3. lock操作:lock函式嘗試將state從0更改為1。如果成功,則傳回一個MutexGuard。如果state已經是1,則執行緒將等待直到它被解鎖。

  4. 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並喚醒任何等待中的執行緒。