多線程背景下,讀請求不斷,寫請求有機會執行嗎?怎么分析?
先用代碼測試下題目當中的情況(完整代碼,可以直接復制用來測試,文末抽獎送書,歡迎參與)
#include <shared_mutex>
#include <thread>
#include <vector>
#include <iostream>
#include <chrono>
std::shared_mutex rw_mutex;
std::string shared_data;
void reader(int id){
while (true)
{
std::shared_lock lock(rw_mutex);
std::cout << "Reader " << id << " reads: " << shared_data << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
void writer(const std::string& new_data){
while (true)
{
std::unique_lock lock(rw_mutex);
shared_data = new_data;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main(){
std::vector<std::thread> readers;
for (int i = 0; i < 5; ++i) {
readers.emplace_back(reader, i);
}
std::thread writer_thread(writer, "Updated Data");
for (auto& t : readers) {
t.join();
}
writer_thread.join();
getchar();
return0;
}
VS2022 執行代碼后,觀察控制臺輸出:
Image
讀線程持續打印 “Reader X reads: Updated Data”(初始值為空)。
寫線程在測試期間未能成功獲取鎖,導致 shared_data 未被更新為 “Updated Data”
我這里代碼故意在讀處理中加了延時,讀線程長時間持有鎖,然后是 5 個讀線程,可以發現寫請求完全得不到機會來處理。
這個現象有個專業名詞叫:寫線程饑餓。
在多線程編程中,讀寫鎖(Read-Write Lock)的機制是否會導致寫請求在持續讀請求下無法執行(即 寫線程饑餓),取決于鎖的具體實現策略和場景特性。以下是逐步分析:
一、讀寫鎖的基本行為
讀寫鎖的核心規則是 “讀共享,寫獨占”:
- 讀鎖(共享鎖):允許多個線程同時讀取資源。
- 寫鎖(獨占鎖):同一時間僅允許一個線程寫入資源,且寫入時會阻塞所有讀鎖和寫鎖。
因此,當讀鎖持續占用時,寫鎖必須等待所有讀鎖釋放后才能獲取。但具體能否執行需結合鎖的調度策略分析。
鎖獲取優先級:
無公平性策略:若讀鎖持續被獲取,寫鎖可能無限等待(饑餓)。
寫優先策略:當寫鎖請求存在時,后續讀鎖會被阻塞,直到寫鎖完成。
公平策略:交替服務讀/寫請求,避免單一方饑餓。
二、寫請求能否執行的場景分析
場景 1:讀寫鎖無公平性策略(常見默認實現)
問題:若讀線程持續獲取讀鎖(無間隙釋放鎖),寫線程可能永遠無法執行。
示例代碼:
std::shared_mutex rw_mutex;
void reader() {
while (true) {
std::shared_lock lock(rw_mutex); // 持續持有讀鎖
// 讀操作...
}
}
void writer() {
std::unique_lock lock(rw_mutex); // 永遠無法獲取寫鎖
// 寫操作...
}
結果:寫線程饑餓。
場景 2:讀寫鎖支持寫優先
策略:當有寫鎖等待時,新讀鎖請求被阻塞,直到寫鎖完成。
實現方式:維護寫等待標記(如計數器),讀鎖獲取前檢查該標記。
結果:寫線程最終能獲得鎖,但可能犧牲讀吞吐量。
場景 3:讀寫鎖支持公平性
策略:通過隊列或時間戳保證讀/寫請求按到達順序交替執行。
示例:Linux 內核的 rw_semaphore 使用公平隊列。
結果:寫線程不會饑餓,但并發讀性能下降。
三、C++標準庫 std::shared_mutex
我們文章開頭的測試出現了寫線程饑餓,那么 std::shared_mutex到底是公平性的還是非公平性的?還是說可以設置呢?
1. C++ 標準的立場
C++ 標準僅定義 std::shared_mutex 的接口和行為規范(如“讀鎖共享,寫鎖獨占”),但 未規定鎖的獲取策略是否公平。這意味著:
公平性(如讀/寫鎖的排隊順序、是否避免饑餓)由具體實現決定。
不同平臺(如 Linux、Windows)或編譯器(如 GCC、Clang、MSVC)可能有不同行為。
2. 常見實現的行為
Linux( GCC/libstdc++)
底層通常基于 pthread_rwlock_t,默認采用 讀優先策略(允許新讀請求搶占等待的寫鎖),可能導致 寫線程饑餓。(不同 Linux 發行版或 glibc 版本可能有不同默認行為,需查閱具體文檔)
示例:若某線程持有讀鎖時,其他讀線程可以繼續獲取讀鎖,而寫線程可能長時間無法獲取鎖。
Windows( MSVC)
底層可能使用 SRWLock(Slim Reader/Writer Lock),其特性是:無優先級保障的競爭式獲取,可能但不必然導致寫饑餓。 當鎖釋放時,等待的讀/寫線程通過競爭獲取鎖,不保證先到先得。
寫線程可能因競爭失敗而饑餓,但實際行為依賴線程調度。
四、解決方案:避免寫饑餓的設計
1. 選擇支持寫優先的讀寫鎖
手動實現(示例):
class FairReadWriteLock {
std::mutex mtx;
std::condition_variable cv;
int readers = 0;
int writers_waiting = 0;
bool writing = false;
public:
void read_lock(){
std::unique_lock lock(mtx);
cv.wait(lock, [this] {
return !writing && writers_waiting == 0; // 無寫者或等待的寫者
});
readers++;
}
void read_unlock(){
std::unique_lock lock(mtx);
if (--readers == 0 && writers_waiting > 0) {
cv.notify_one(); // 喚醒寫者
}
}
void write_lock(){
std::unique_lock lock(mtx);
writers_waiting++;
cv.wait(lock, [this] {
return !writing && readers == 0; // 無活動的讀/寫者
});
writers_waiting--;
writing = true;
}
void write_unlock(){
std::unique_lock lock(mtx);
writing = false;
cv.notify_all(); // 喚醒所有讀者和寫者
}
};
效果:當有寫者等待時,新讀者被阻塞,確保寫者最終執行。
2. 使用操作系統級公平鎖
Linux:通過 pthread_rwlockattr_setkind_np 設置 PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP。
pthread_rwlock_t rwlock;
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&rwlock, &attr);
3. 業務層限流
在讀邏輯中插入條件檢查,主動釋放鎖允許寫操作:(此方法依賴線程調度器實現,可能緩解但無法徹底避免饑餓)
void reader() {
while (true) {
{
std::shared_lock lock(rw_mutex);
// 讀操作...
}
std::this_thread::yield(); // 主動讓出CPU,增加寫者機會
}
}
六、總結
寫請求能否執行:取決于鎖實現的公平性策略。
無公平性策略 → 可能饑餓(需業務層干預)。
寫優先或公平隊列 → 可避免饑餓。