在多執行緒程式設計中,鎖定機制是確保資料一致性的關鍵。傳統鎖機制常因效能瓶頸而受限,本文介紹的 Parking Lot–Based Locks 和 Sequence Lock 提供了更優異的解決方案。前者以極小尺寸的互斥鎖提升效能,後者則以非阻塞方式更新資料,適用於高平行環境。兩種機制各有千秋,選擇取決於具體應用場景的需求。

高效能鎖定機制:Parking Lot–Based Locks 與 Sequence Lock

在現代多執行緒程式設計中,高效能的鎖定機制對於提升系統效能至關重要。本文將探討兩種先進的鎖定機制:Parking Lot–Based Locks 和 Sequence Lock,並分析其設計原理、實作方式以及應用場景。

Parking Lot–Based Locks:高效的小型互斥鎖

Parking Lot–Based Locks 是一種根據全域資料結構的鎖定機制,能夠實作極小尺寸的互斥鎖。該機制透過將等待佇列存放在全域的 HashMap 中,僅在互斥鎖內部保留一或兩個位元,從而實作高效的鎖定操作。

實作原理

  1. 全域資料結構:使用 HashMap 將記憶體位址對映到等待該互斥鎖的執行緒佇列。
  2. 小型互斥鎖:互斥鎖本身僅佔用一個位元組,甚至可以嵌入指標的未使用位元中。
  3. 擴充套件性:除了互斥鎖外,還能追蹤條件變數和其他同步原語的等待佇列。

優點

  • 高效能:透過減少互斥鎖的大小,實作幾乎無額外成本的細粒度鎖定。
  • 靈活性:可廣泛應用於各種同步場景,包括互斥鎖和條件變數。

實際應用

Parking Lot–Based Locks 最早在 2015 年被應用於 WebKit,用於鎖定 JavaScript 物件。其高效能的特性啟發了其他實作,如流行的 Rust crate parking_lot

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;

struct ParkingLot {
    mutex: Arc<Mutex<()>>,
    state: AtomicUsize,
}

impl ParkingLot {
    fn new() -> Self {
        ParkingLot {
            mutex: Arc::new(Mutex::new(())),
            state: AtomicUsize::new(0),
        }
    }

    fn lock(&self) {
        // 使用 CAS 操作嘗試取得鎖
        if self.state.compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed).is_err() {
            // 取得鎖失敗,將執行緒加入等待佇列
            let mutex = self.mutex.clone();
            let _guard = mutex.lock().unwrap();
            // 重新嘗試取得鎖
        }
    }

    fn unlock(&self) {
        self.state.store(0, Ordering::Release);
        // 通知等待佇列中的執行緒
    }
}

#### 內容解密:
1. **使用 AtomicUsize 實作鎖狀態**:`state` 欄位用於表示鎖的狀態,0 表示未鎖定,1 表示鎖定。
2. **CAS 操作取得鎖**:使用 `compare_exchange` 方法嘗試將鎖狀態從 0 修改為 1,若失敗則表示鎖已被佔用。
3. **等待佇列機制**:當鎖取得失敗時,將執行緒加入等待佇列,並重新嘗試取得鎖。
4. **解鎖操作**:將鎖狀態設為 0,並通知等待佇列中的執行緒。

Sequence Lock:非阻塞的資料更新機制

Sequence Lock 是一種用於原子更新較大資料結構的機制,無需使用傳統的阻塞式鎖。它利用一個原子計數器來標示資料是否正在被更新。

實作原理

  1. 原子計數器:使用一個原子變數作為計數器,奇數表示資料正在更新,偶數表示資料已準備好。
  2. 寫入操作:寫入執行緒在更新資料前後分別將計數器加一,使其從偶數變為奇數再變為新的偶數。
  3. 讀取操作:讀取執行緒在讀取資料前後分別讀取計數器,若兩次讀取結果相同且為偶數,則表示讀取的資料是有效的。

優點

  • 非阻塞:讀取執行緒不會阻塞寫入執行緒,提升了系統的平行度。
  • 適用於分享記憶體:由於讀取操作僅需讀取許可權,且不涉及指標操作,適合用於程式間的分享記憶體。

實際應用

Sequence Lock 在作業系統核心和嵌入式系統中廣泛應用。例如,Linux 核心利用該機制為程式提供高效的時間戳。

use std::sync::atomic::{AtomicUsize, Ordering};

struct SequenceLock<T> {
    data: T,
    sequence: AtomicUsize,
}

impl<T> SequenceLock<T> {
    fn new(data: T) -> Self {
        SequenceLock {
            data,
            sequence: AtomicUsize::new(0),
        }
    }

    fn write(&self, new_data: T) {
        self.sequence.fetch_add(1, Ordering::SeqCst); // 變為奇數
        self.data = new_data;
        self.sequence.fetch_add(1, Ordering::SeqCst); // 變為偶數
    }

    fn read(&self) -> Option<T>
    where
        T: Clone,
    {
        let before = self.sequence.load(Ordering::SeqCst);
        if before % 2 != 0 {
            return None; // 資料正在更新中
        }
        let data_copy = self.data.clone();
        let after = self.sequence.load(Ordering::SeqCst);
        if before == after {
            Some(data_copy)
        } else {
            None // 資料在讀取過程中被更新
        }
    }
}

#### 內容解密:
1. **資料與計數器**:`data` 欄位儲存實際資料,`sequence` 欄位作為計數器。
2. **寫入操作**:寫入前後分別更新計數器,確保讀取執行緒能檢測到更新過程。
3. **讀取操作**:透過比較讀取前後的計數器值,判斷資料是否在讀取過程中被更新。
4. **非阻塞特性**:讀取操作不會阻塞寫入操作,適合高平行場景。

教學與知識分享

除了研究並實作新的平行資料結構外,創作教學材料、分享知識也是一條有意義的道路。目前,針對平行程式設計和原子操作的學習資源仍相對匱乏。希望本文能為讀者提供有價值的參考,並激發更多人投入到平行程式設計的學習與實踐中。

隨著平行程式設計技術的持續發展,我們可以預見更多高效的同步機制和資料結構將被提出和應用。開發者應持續關注最新的研究成果和技術趨勢,不斷提升自己的技術水平,以應對日益複雜的平行程式設計挑戰。

參考資料

Parking Lot–Based Locks 流程圖

  graph TD;
    B[B]
    A[執行緒請求鎖] --> B{鎖是否可用?};
    B -- 是 --> C[取得鎖];
    B -- 否 --> D[加入等待佇列];
    C --> E[執行臨界區程式碼];
    E --> F[釋放鎖];
    D --> G[等待喚醒];
    G --> B;
    F --> H[通知等待佇列];

圖表翻譯: 此圖示展示了 Parking Lot–Based Locks 的工作流程:

  1. 執行緒請求鎖時,首先檢查鎖是否可用。
  2. 若鎖可用,則直接取得鎖並執行臨界區程式碼。
  3. 若鎖不可用,則將執行緒加入等待佇列。
  4. 當鎖被釋放時,通知等待佇列中的執行緒重新嘗試取得鎖。

Sequence Lock 讀取流程圖

  graph TD;
    C[C]
    F[F]
    A[開始讀取] --> B[讀取計數器 before];
    B --> C{計數器 before 為偶數?};
    C -- 是 --> D[讀取資料];
    D --> E[讀取計數器 after];
    E --> F{計數器 before == after?};
    F -- 是 --> G[傳回資料];
    F -- 否 --> H[重試讀取];
    C -- 否 --> I[重試讀取];

圖表翻譯: 此圖示展示了 Sequence Lock 的讀取流程:

  1. 讀取操作開始前,先讀取計數器的值(before)。
  2. 若計數器為偶數,則讀取資料並再次讀取計數器(after)。
  3. 比較 before 和 after,若相同則傳回資料。
  4. 若計數器為奇數或 before 與 after 不同,表示資料在讀取過程中被更新,需重試讀取。

深入理解原子操作與平行程式設計

在現代軟體開發中,尤其是在系統程式設計和高效能運算領域,原子操作(Atomic Operations)和平行程式設計(Concurrent Programming)扮演著至關重要的角色。本文將探討原子操作的基本原理、應用場景,以及如何在 Rust 語言中有效地利用原子操作來構建高效的平行程式。

原子操作基礎

原子操作是指不可分割的操作,一旦開始執行,就會一直執行到完成,中間不會被其他操作幹擾。在多執行緒環境中,原子操作對於確保資料的一致性和正確性至關重要。

為什麼需要原子操作?

  1. 資料競爭(Data Races):在多執行緒環境中,如果多個執行緒同時存取和修改分享資料,就可能發生資料競爭,導致未定義行為。原子操作可以避免這種情況。
  2. 同步機制:原子操作提供了一種輕量級的同步機制,可以用於實作鎖(Locks)、訊號量(Semaphores)等同步原語。

Rust 中的原子操作

Rust 提供了豐富的原子操作支援,透過 std::sync::atomic 模組提供各種原子型別和操作。

常見的原子型別

  1. AtomicBool: 用於原子布林值操作。
  2. AtomicPtr: 用於原子指標操作。
  3. AtomicUsize, AtomicIsize: 用於原子無符號整數和有符號整數操作。

原子操作示例

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;

fn main() {
    let counter = Arc::new(AtomicUsize::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            for _ in 0..1000 {
                counter_clone.fetch_add(1, Ordering::SeqCst);
            }
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("最終計數: {}", counter.load(Ordering::SeqCst));
}

內容解密:

  1. 我們使用 Arc(原子參考計數)來分享 AtomicUsize 例項,讓多個執行緒可以安全地存取和修改計數器。
  2. fetch_add 方法用於原子地增加計數器的值,Ordering::SeqCst 確保操作的順序一致性。
  3. 主執行緒等待所有子執行緒完成,然後列印最終的計數結果。

記憶體排序(Memory Ordering)

在進行原子操作時,記憶體排序是一個重要的概念,它決定了不同執行緒對分享變數的存取順序。

記憶體排序選項

  1. Ordering::Relaxed: 最寬鬆的排序約束,只保證操作的原子性。
  2. Ordering::Acquire: 用於讀取操作,確保後續操作不會被重排序到該操作之前。
  3. Ordering::Release: 用於寫入操作,確保之前的寫入操作不會被重排序到該操作之後。
  4. Ordering::AcqRel: 結合 AcquireRelease,用於讀取-修改-寫入操作。
  5. Ordering::SeqCst: 最嚴格的排序約束,確保所有執行緒對所有操作的順序達成一致。

構建自定義同步原語

利用原子操作,我們可以構建各種同步原語,如鎖、訊號量和通道。

示例:自定義 SpinLock

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;

struct SpinLock {
    locked: AtomicBool,
}

impl SpinLock {
    fn new() -> Self {
        SpinLock {
            locked: AtomicBool::new(false),
        }
    }

    fn lock(&self) {
        loop {
            if self.locked.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed).is_ok() {
                break;
            }
            // 自旋等待
            thread::yield_now();
        }
    }

    fn unlock(&self) {
        self.locked.store(false, Ordering::Release);
    }
}

fn main() {
    let lock = Arc::new(SpinLock::new());
    let mut handles = vec![];

    for _ in 0..10 {
        let lock_clone = Arc::clone(&lock);
        let handle = thread::spawn(move || {
            lock_clone.lock();
            println!("執行緒獲得鎖");
            lock_clone.unlock();
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
}

內容解密:

  1. 自定義 SpinLock 結構體包含一個 AtomicBool 欄位,用於表示鎖的狀態。
  2. lock 方法使用 compare_exchange 進行原子交換,嘗試取得鎖。如果取得失敗,則執行緒讓出 CPU 時間片,進行自旋等待。
  3. unlock 方法簡單地將鎖的狀態設為 false,並使用 Release 排序確保之前的寫入操作不會被重排序。