在多執行緒程式設計中,鎖定機制是確保資料一致性的關鍵。傳統鎖機制常因效能瓶頸而受限,本文介紹的 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 中,僅在互斥鎖內部保留一或兩個位元,從而實作高效的鎖定操作。
實作原理
- 全域資料結構:使用 HashMap 將記憶體位址對映到等待該互斥鎖的執行緒佇列。
- 小型互斥鎖:互斥鎖本身僅佔用一個位元組,甚至可以嵌入指標的未使用位元中。
- 擴充套件性:除了互斥鎖外,還能追蹤條件變數和其他同步原語的等待佇列。
優點
- 高效能:透過減少互斥鎖的大小,實作幾乎無額外成本的細粒度鎖定。
- 靈活性:可廣泛應用於各種同步場景,包括互斥鎖和條件變數。
實際應用
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 是一種用於原子更新較大資料結構的機制,無需使用傳統的阻塞式鎖。它利用一個原子計數器來標示資料是否正在被更新。
實作原理
- 原子計數器:使用一個原子變數作為計數器,奇數表示資料正在更新,偶數表示資料已準備好。
- 寫入操作:寫入執行緒在更新資料前後分別將計數器加一,使其從偶數變為奇數再變為新的偶數。
- 讀取操作:讀取執行緒在讀取資料前後分別讀取計數器,若兩次讀取結果相同且為偶數,則表示讀取的資料是有效的。
優點
- 非阻塞:讀取執行緒不會阻塞寫入執行緒,提升了系統的平行度。
- 適用於分享記憶體:由於讀取操作僅需讀取許可權,且不涉及指標操作,適合用於程式間的分享記憶體。
實際應用
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. **非阻塞特性**:讀取操作不會阻塞寫入操作,適合高平行場景。
教學與知識分享
除了研究並實作新的平行資料結構外,創作教學材料、分享知識也是一條有意義的道路。目前,針對平行程式設計和原子操作的學習資源仍相對匱乏。希望本文能為讀者提供有價值的參考,並激發更多人投入到平行程式設計的學習與實踐中。
隨著平行程式設計技術的持續發展,我們可以預見更多高效的同步機制和資料結構將被提出和應用。開發者應持續關注最新的研究成果和技術趨勢,不斷提升自己的技術水平,以應對日益複雜的平行程式設計挑戰。
參考資料
- WebKit 部落格文章:Locking in WebKit
parking_lotcrate 檔案:parking_lot crate- Wikipedia 文章:Linux’s Seqlock
- Rust RFC 3301:AtomicPerByte
seqlockcrate 檔案:seqlock crate
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 的工作流程:
- 執行緒請求鎖時,首先檢查鎖是否可用。
- 若鎖可用,則直接取得鎖並執行臨界區程式碼。
- 若鎖不可用,則將執行緒加入等待佇列。
- 當鎖被釋放時,通知等待佇列中的執行緒重新嘗試取得鎖。
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 的讀取流程:
- 讀取操作開始前,先讀取計數器的值(before)。
- 若計數器為偶數,則讀取資料並再次讀取計數器(after)。
- 比較 before 和 after,若相同則傳回資料。
- 若計數器為奇數或 before 與 after 不同,表示資料在讀取過程中被更新,需重試讀取。
深入理解原子操作與平行程式設計
在現代軟體開發中,尤其是在系統程式設計和高效能運算領域,原子操作(Atomic Operations)和平行程式設計(Concurrent Programming)扮演著至關重要的角色。本文將探討原子操作的基本原理、應用場景,以及如何在 Rust 語言中有效地利用原子操作來構建高效的平行程式。
原子操作基礎
原子操作是指不可分割的操作,一旦開始執行,就會一直執行到完成,中間不會被其他操作幹擾。在多執行緒環境中,原子操作對於確保資料的一致性和正確性至關重要。
為什麼需要原子操作?
- 資料競爭(Data Races):在多執行緒環境中,如果多個執行緒同時存取和修改分享資料,就可能發生資料競爭,導致未定義行為。原子操作可以避免這種情況。
- 同步機制:原子操作提供了一種輕量級的同步機制,可以用於實作鎖(Locks)、訊號量(Semaphores)等同步原語。
Rust 中的原子操作
Rust 提供了豐富的原子操作支援,透過 std::sync::atomic 模組提供各種原子型別和操作。
常見的原子型別
AtomicBool: 用於原子布林值操作。AtomicPtr: 用於原子指標操作。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));
}
內容解密:
- 我們使用
Arc(原子參考計數)來分享AtomicUsize例項,讓多個執行緒可以安全地存取和修改計數器。 fetch_add方法用於原子地增加計數器的值,Ordering::SeqCst確保操作的順序一致性。- 主執行緒等待所有子執行緒完成,然後列印最終的計數結果。
記憶體排序(Memory Ordering)
在進行原子操作時,記憶體排序是一個重要的概念,它決定了不同執行緒對分享變數的存取順序。
記憶體排序選項
Ordering::Relaxed: 最寬鬆的排序約束,只保證操作的原子性。Ordering::Acquire: 用於讀取操作,確保後續操作不會被重排序到該操作之前。Ordering::Release: 用於寫入操作,確保之前的寫入操作不會被重排序到該操作之後。Ordering::AcqRel: 結合Acquire和Release,用於讀取-修改-寫入操作。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();
}
}
內容解密:
- 自定義
SpinLock結構體包含一個AtomicBool欄位,用於表示鎖的狀態。 lock方法使用compare_exchange進行原子交換,嘗試取得鎖。如果取得失敗,則執行緒讓出 CPU 時間片,進行自旋等待。unlock方法簡單地將鎖的狀態設為false,並使用Release排序確保之前的寫入操作不會被重排序。