悲觀鎖
悲觀鎖認為在并發環境中,數據隨時可能被其他線程修改,因此在訪問數據之前會先加鎖,以防止其他線程對數據進行修改。常見的悲觀鎖實現有:
1.互斥鎖
原理:互斥鎖是一種最基本的鎖類型,同一時間只允許一個線程訪問共享資源。當一個線程獲取到互斥鎖后,其他線程如果想要訪問該資源,就必須等待鎖被釋放。
應用場景:適用于寫操作頻繁的場景,如數據庫中的數據更新操作。在 C++ 中可以使用?std::mutex
?來實現互斥鎖,示例代碼如下:
#include <iostream>
#include <mutex>
#include <thread>std::mutex mtx;
int sharedResource = 0;void increment() {std::lock_guard<std::mutex> lock(mtx);sharedResource++;
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Shared resource: " << sharedResource << std::endl;return 0;
}
2.讀寫鎖
原理:讀寫鎖允許多個線程同時進行讀操作,但在進行寫操作時,會獨占資源,不允許其他線程進行讀或寫操作。讀寫鎖分為讀鎖和寫鎖,多個線程可以同時獲取讀鎖,但寫鎖是排他的。
應用場景:適用于讀多寫少的場景,如緩存系統。在 C++ 中可以使用?std::shared_mutex?來實現讀寫鎖,示例代碼如下:?
#include <iostream>
#include <shared_mutex>
#include <thread>std::shared_mutex rwMutex;
int sharedData = 0;void readData() {std::shared_lock<std::shared_mutex> lock(rwMutex);std::cout << "Read data: " << sharedData << std::endl;
}void writeData() {std::unique_lock<std::shared_mutex> lock(rwMutex);sharedData++;std::cout << "Write data: " << sharedData << std::endl;
}int main() {std::thread t1(readData);std::thread t2(writeData);t1.join();t2.join();return 0;
}
樂觀鎖
樂觀鎖是一種在多線程環境中避免阻塞的同步技術,它假設大部分操作是不會發生沖突的,因此在操作數據時不會直接加鎖,而是通過檢查數據是否發生了變化來決定是否提交。如果在提交數據時發現數據已被其他線程修改,則會放棄當前操作,重新讀取數據并重試。
應用場景:適用于讀多寫少、沖突較少的場景,如電商系統中的庫存管理。
在 C++ 中,樂觀鎖的實現通常依賴于版本號或時間戳的機制。每個線程在操作數據時,會記錄數據的版本或時間戳,操作完成后再通過比較版本號或時間戳來判斷是否發生了沖突。
下面是一個使用版本號實現樂觀鎖的簡單示例代碼:
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>// 共享數據結構
struct SharedData {int value; // 數據的實際值std::atomic<int> version; // 數據的版本號,用于檢查是否發生了修改
};// 線程安全的樂觀鎖實現
bool optimisticLockUpdate(SharedData& data, int expectedVersion, int newValue) {// 檢查數據的版本號是否與預期一致if (data.version.load() == expectedVersion) {// 進行數據更新data.value = newValue;// 增加版本號data.version.fetch_add(1, std::memory_order_relaxed);return true; // 成功提交更新}return false; // 數據版本不一致,操作失敗
}void threadFunction(SharedData& data, int threadId) {int expectedVersion = data.version.load();int newValue = threadId * 10;std::cout << "Thread " << threadId << " starting with version " << expectedVersion << "...\n";std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模擬工作// 嘗試更新數據if (optimisticLockUpdate(data, expectedVersion, newValue)) {std::cout << "Thread " << threadId << " successfully updated value to " << newValue << "\n";} else {std::cout << "Thread " << threadId << " failed to update (version mismatch)\n";}
}int main() {// 初始化共享數據,值為 0,版本號為 0SharedData data{0, 0};// 啟動多個線程進行樂觀鎖測試std::thread t1(threadFunction, std::ref(data), 1);//std::ref(data) 將 data 包裝成一個引用包裝器,確保 data 在傳遞給函數時以引用的方式傳遞,而不是被復制。std::thread t2(threadFunction, std::ref(data), 2);std::thread t3(threadFunction, std::ref(data), 3);t1.join();t2.join();t3.join();std::cout << "Final value: " << data.value << ", Final version: " << data.version.load() << "\n";return 0;
}
原子鎖
原子鎖是一種基于原子操作(如CAS、test_and_set)的鎖機制。與傳統的基于互斥量(如 std::mutex
)的鎖不同,原子鎖依賴于硬件提供的原子操作,允許對共享資源的訪問進行同步,且通常比傳統鎖更加高效。它通過原子操作保證對共享資源的獨占訪問,而不需要顯式的線程調度。
原子鎖的適用場景:
1.簡單數據類型:原子鎖最常用于鎖定簡單的基礎數據類型,例如整數、布爾值、指針等。通過原子操作,多個線程可以安全地對這些數據進行讀寫,而不會發生數據競爭。
示例:std::atomic<int>,?std::atomic<bool>,?std::atomic<long long>
2.計數器、標志位:當需要在多線程中維護計數器、標志位或狀態變量時,原子操作非常合適。例如,當多個線程需要遞增計數器時,可以用原子操作避免使用傳統的互斥鎖。
示例:使用?std::atomic<int>?來維護線程安全的計數器。
注:原子鎖通常不能鎖容器類型。
什么是原子操作?
原子操作是指不可分割的操作,在執行過程中不會被中斷或干擾。原子操作保證了操作的完整性,要么完全執行,要么完全不執行,避免了在操作過程中被線程切換打斷,從而避免了數據競爭和不一致的情況。
1.自旋鎖
什么是自旋鎖?
自旋鎖是一種使用原子操作來檢測鎖是否可用的鎖機制。自旋鎖是一種忙等待的鎖,當線程嘗試獲取鎖失敗時,會不斷地檢查鎖的狀態,直到成功獲取鎖。?
在 C++ 中,可以使用?std::atomic_flag
?結合?test_and_set
?操作來實現一個簡單的自旋鎖:
test_and_set
?是一個原子操作,它會檢查一個布爾標志的值,然后將該標志設置為?true
。整個操作過程是不可分割的,即不會被其他線程的操作打斷。這個布爾標志通常被用作鎖,線程通過檢查并設置這個標志來嘗試獲取鎖。
工作原理
- 檢查標志狀態:線程首先檢查布爾標志的當前值。
- 設置標志為 true:如果標志當前為 false,表示鎖未被占用,線程將標志設置為 true,表示成功獲取到鎖;如果標志當前為 true,表示鎖已被其他線程占用,線程未能獲取到鎖。
- 返回舊值:test_and_set 操作會返回標志的舊值。線程可以根據這個返回值判斷是否成功獲取到鎖。如果返回 false,說明成功獲取到鎖;如果返回 true,則需要等待鎖被釋放后再次嘗試獲取。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>std::atomic_flag lock = ATOMIC_FLAG_INIT;// 自旋鎖類
class SpinLock {
public:void lock() {// 持續嘗試獲取鎖,直到成功while (lock.test_and_set(std::memory_order_acquire)) {// 自旋等待}}void unlock() {// 釋放鎖,將標志設置為 falselock.clear(std::memory_order_release);}
};SpinLock spinLock;
int sharedResource = 0;// 線程函數
void worker() {for (int i = 0; i < 100000; ++i) {spinLock.lock();++sharedResource;spinLock.unlock();}
}int main() {std::vector<std::thread> threads;// 創建多個線程for (int i = 0; i < 4; ++i) {threads.emplace_back(worker);}// 等待所有線程完成for (auto& thread : threads) {thread.join();}std::cout << "Shared resource value: " << sharedResource << std::endl;return 0;
}
自旋鎖優點:
-
無上下文切換:自旋鎖不會引起線程掛起,因此避免了上下文切換的開銷。在鎖競爭較輕時,自旋鎖可以高效地工作。
-
簡單高效:實現簡單,且不依賴操作系統調度,適合鎖競爭不嚴重的場景。
自旋鎖缺點:
-
CPU資源浪費:如果鎖被占用,自旋鎖會不斷地循環檢查鎖的狀態,浪費 CPU 時間,尤其是在鎖持有時間較長時,可能導致性能問題。
-
不適合鎖競爭場景:當有大量線程競爭同一個鎖時,自旋鎖的性能將大幅下降,因為大部分時間都在自旋,浪費了 CPU 資源。
自旋鎖的適用場景:
-
短時間鎖競爭:自旋鎖適用于臨界區代碼執行時間非常短的情況。如果鎖持有時間較長,使用自旋鎖就不合適了。
-
鎖競爭較輕:在多線程程序中,如果線程數量較少且資源競爭較少,自旋鎖可以有效減少線程上下文切換,提升性能。
-
實時系統或高性能系統:在某些對延遲非常敏感的應用場景中,自旋鎖可以通過減少上下文切換來提供更低的延遲。
總結:自旋鎖是一種簡單且高效的鎖機制,通過原子操作避免了線程上下文切換,適合用于短時間鎖競爭和低延遲要求的場景。在鎖競爭激烈或鎖持有時間較長時,自旋鎖的性能會受到影響,這時傳統的互斥鎖(如 std::mutex
)可能更為合適。
遞歸鎖
在 C++ 中,遞歸鎖也被稱為可重入鎖,它是一種特殊的鎖機制,允許同一個線程多次獲取同一把鎖而不會產生死鎖。
原理
普通的互斥鎖(如?std::mutex
)不允許同一個線程在已經持有鎖的情況下再次獲取該鎖,否則會導致死鎖。因為當線程第一次獲取鎖后,鎖處于被占用狀態,再次嘗試獲取時,由于鎖未被釋放,線程會被阻塞,而該線程又因為被阻塞無法釋放鎖,從而陷入死循環。
遞歸鎖則不同,它內部維護了一個計數器和一個持有鎖的線程標識。當一個線程第一次獲取遞歸鎖時,計數器加 1,同時記錄該線程的標識。如果該線程再次請求獲取同一把鎖,計數器會繼續加 1,而不會被阻塞。當線程釋放鎖時,計數器減 1,直到計數器為 0 時,鎖才會真正被釋放,其他線程才可以獲取該鎖。
應用場景:
- 遞歸調用:在遞歸函數中,如果需要對共享資源進行保護,使用遞歸鎖可以避免死鎖問題。例如,在一個遞歸遍歷樹結構的函數中,可能需要對樹節點的某些屬性進行修改,此時可以使用遞歸鎖來保證線程安全。
- 嵌套鎖:當代碼中存在多層嵌套的鎖獲取操作,且這些操作可能由同一個線程執行時,遞歸鎖可以避免死鎖。例如,一個函數內部調用了另一個函數,這兩個函數都需要獲取同一把鎖。
注意事項:
1. 性能開銷
遞歸鎖的實現比普通互斥鎖更為復雜。普通互斥鎖只需簡單地標記鎖的占用狀態,當一個線程請求鎖時,檢查該狀態并進行相應操作。而遞歸鎖除了要維護鎖的占用狀態,還需要記錄持有鎖的線程標識以及一個計數器,用于跟蹤同一個線程獲取鎖的次數。每次獲取和釋放鎖時,都需要對這些額外信息進行更新和檢查,這無疑增加了系統的開銷。
- 時間開銷:由于額外的狀態檢查和更新操作,遞歸鎖的加鎖和解鎖操作通常比普通互斥鎖更耗時。在高并發、對性能要求極高的場景下,頻繁使用遞歸鎖可能會成為性能瓶頸。
- 資源開銷:記錄線程標識和計數器需要額外的內存空間,雖然這部分開銷相對較小,但在資源受限的系統中,也可能會產生一定的影響。
建議:在不需要遞歸獲取鎖的場景下,應優先使用普通互斥鎖(如 std::mutex)。
2. 死鎖風險
雖然遞歸鎖允許同一個線程多次獲取同一把鎖而不會死鎖,但如果在遞歸調用過程中,鎖的獲取和釋放邏輯出現錯誤,仍然可能導致死鎖。例如,在遞歸函數中,獲取鎖后在某些條件下沒有正確釋放鎖就進行了遞歸調用,可能會導致鎖無法正常釋放,其他線程請求該鎖時就會陷入死鎖。
#include <iostream>
#include <thread>
#include <mutex>std::recursive_mutex recMutex;void faultyRecursiveFunction(int n) {if (n == 0) return;std::lock_guard<std::recursive_mutex> lock(recMutex);std::cout << "Recursive call: " << n << std::endl;if (n == 2) {// 錯誤:沒有釋放鎖就返回,可能導致死鎖return;}faultyRecursiveFunction(n - 1);
}int main() {std::thread t(faultyRecursiveFunction, 3);t.join();return 0;
}
3.不同遞歸鎖之間的交叉鎖定
當存在多個遞歸鎖時,如果不同線程以不同的順序獲取這些鎖,就可能會產生死鎖。例如,線程 A 先獲取了遞歸鎖 L1,然后嘗試獲取遞歸鎖 L2;而線程 B 先獲取了遞歸鎖 L2,然后嘗試獲取遞歸鎖 L1。此時,兩個線程都在等待對方釋放鎖,從而陷入死鎖狀態。
在 C++ 標準庫中,std::recursive_mutex
?是遞歸鎖的實現。以下是一個簡單的示例代碼:
#include <iostream>
#include <thread>
#include <mutex>std::recursive_mutex recMutex;// 遞歸函數,多次獲取遞歸鎖
void recursiveFunction(int n) {if (n == 0) return;// 加鎖std::lock_guard<std::recursive_mutex> lock(recMutex);std::cout << "Recursive call: " << n << std::endl;// 遞歸調用recursiveFunction(n - 1);// 鎖在離開作用域時自動釋放
}int main() {std::thread t(recursiveFunction, 3);t.join();return 0;
}
什么是鎖的重入與不可重入?
可重入鎖也叫遞歸鎖,允許同一個線程在已經持有該鎖的情況下,再次獲取同一把鎖而不會產生死鎖。可重入鎖內部會維護一個持有鎖的線程標識和一個計數器。當線程第一次獲取鎖時,會記錄該線程的標識,并將計數器初始化為 1。如果該線程再次請求獲取同一把鎖,鎖會檢查請求線程的標識是否與當前持有鎖的線程標識相同,如果相同,則將計數器加 1,而不會阻塞該線程。釋放鎖時,計數器減 1,直到計數器為 0 時,鎖才會釋放,其他線程才可以獲取該鎖。
不可重入鎖不允許同一個線程在已經持有該鎖的情況下再次獲取同一把鎖。如果一個線程已經持有了不可重入鎖,再次請求獲取該鎖時,會導致線程阻塞,進而可能產生死鎖。不可重入鎖只關注鎖的占用狀態,不記錄持有鎖的線程標識和獲取鎖的次數。當一個線程請求獲取鎖時,鎖會檢查其是否已被占用,如果已被占用,無論請求線程是否就是持有鎖的線程,都會將該線程阻塞。