多線程背景下,讀請求不斷,寫請求有機(jī)會執(zhí)行嗎?怎么分析?
先用代碼測試下題目當(dāng)中的情況(完整代碼,可以直接復(fù)制用來測試,文末抽獎送書,歡迎參與)
#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 執(zhí)行代碼后,觀察控制臺輸出:
Image
讀線程持續(xù)打印 “Reader X reads: Updated Data”(初始值為空)?!?/p>
寫線程在測試期間未能成功獲取鎖,導(dǎo)致 shared_data 未被更新為 “Updated Data”
我這里代碼故意在讀處理中加了延時,讀線程長時間持有鎖,然后是 5 個讀線程,可以發(fā)現(xiàn)寫請求完全得不到機(jī)會來處理?!?/p>
這個現(xiàn)象有個專業(yè)名詞叫:寫線程饑餓?!?/p>
在多線程編程中,讀寫鎖(Read-Write Lock)的機(jī)制是否會導(dǎo)致寫請求在持續(xù)讀請求下無法執(zhí)行(即 寫線程饑餓),取決于鎖的具體實現(xiàn)策略和場景特性。以下是逐步分析:
一、讀寫鎖的基本行為
讀寫鎖的核心規(guī)則是 “讀共享,寫?yīng)氄肌保骸?/p>
- 讀鎖(共享鎖):允許多個線程同時讀取資源。
- 寫鎖(獨占鎖):同一時間僅允許一個線程寫入資源,且寫入時會阻塞所有讀鎖和寫鎖。
因此,當(dāng)讀鎖持續(xù)占用時,寫鎖必須等待所有讀鎖釋放后才能獲取。但具體能否執(zhí)行需結(jié)合鎖的調(diào)度策略分析?!?/p>
鎖獲取優(yōu)先級:
無公平性策略:若讀鎖持續(xù)被獲取,寫鎖可能無限等待(饑餓)。
寫優(yōu)先策略:當(dāng)寫鎖請求存在時,后續(xù)讀鎖會被阻塞,直到寫鎖完成。
公平策略:交替服務(wù)讀/寫請求,避免單一方饑餓。
二、寫請求能否執(zhí)行的場景分析
場景 1:讀寫鎖無公平性策略(常見默認(rèn)實現(xiàn))
問題:若讀線程持續(xù)獲取讀鎖(無間隙釋放鎖),寫線程可能永遠(yuǎn)無法執(zhí)行。
示例代碼:
std::shared_mutex rw_mutex;
void reader() {
while (true) {
std::shared_lock lock(rw_mutex); // 持續(xù)持有讀鎖
// 讀操作...
}
}
void writer() {
std::unique_lock lock(rw_mutex); // 永遠(yuǎn)無法獲取寫鎖
// 寫操作...
}
結(jié)果:寫線程饑餓。
場景 2:讀寫鎖支持寫優(yōu)先
策略:當(dāng)有寫鎖等待時,新讀鎖請求被阻塞,直到寫鎖完成。
實現(xiàn)方式:維護(hù)寫等待標(biāo)記(如計數(shù)器),讀鎖獲取前檢查該標(biāo)記。
結(jié)果:寫線程最終能獲得鎖,但可能犧牲讀吞吐量?!?/p>
場景 3:讀寫鎖支持公平性
策略:通過隊列或時間戳保證讀/寫請求按到達(dá)順序交替執(zhí)行。
示例:Linux 內(nèi)核的 rw_semaphore 使用公平隊列。
結(jié)果:寫線程不會饑餓,但并發(fā)讀性能下降?!?/p>
三、C++標(biāo)準(zhǔn)庫 std::shared_mutex
我們文章開頭的測試出現(xiàn)了寫線程饑餓,那么 std::shared_mutex到底是公平性的還是非公平性的?還是說可以設(shè)置呢?
1. C++ 標(biāo)準(zhǔn)的立場
C++ 標(biāo)準(zhǔn)僅定義 std::shared_mutex 的接口和行為規(guī)范(如“讀鎖共享,寫鎖獨占”),但 未規(guī)定鎖的獲取策略是否公平。這意味著:
公平性(如讀/寫鎖的排隊順序、是否避免饑餓)由具體實現(xiàn)決定?!?/p>
不同平臺(如 Linux、Windows)或編譯器(如 GCC、Clang、MSVC)可能有不同行為?!?/p>
2. 常見實現(xiàn)的行為
Linux( GCC/libstdc++)
底層通?;?nbsp;pthread_rwlock_t,默認(rèn)采用 讀優(yōu)先策略(允許新讀請求搶占等待的寫鎖),可能導(dǎo)致 寫線程饑餓。(不同 Linux 發(fā)行版或 glibc 版本可能有不同默認(rèn)行為,需查閱具體文檔)
示例:若某線程持有讀鎖時,其他讀線程可以繼續(xù)獲取讀鎖,而寫線程可能長時間無法獲取鎖?!?/p>
Windows( MSVC)
底層可能使用 SRWLock(Slim Reader/Writer Lock),其特性是:無優(yōu)先級保障的競爭式獲取,可能但不必然導(dǎo)致寫?zhàn)囸I。 當(dāng)鎖釋放時,等待的讀/寫線程通過競爭獲取鎖,不保證先到先得。
寫線程可能因競爭失敗而饑餓,但實際行為依賴線程調(diào)度。
四、解決方案:避免寫?zhàn)囸I的設(shè)計
1. 選擇支持寫優(yōu)先的讀寫鎖
手動實現(xiàn)(示例):
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(); // 喚醒所有讀者和寫者
}
};
效果:當(dāng)有寫者等待時,新讀者被阻塞,確保寫者最終執(zhí)行。
2. 使用操作系統(tǒng)級公平鎖
Linux:通過 pthread_rwlockattr_setkind_np 設(shè)置 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. 業(yè)務(wù)層限流
在讀邏輯中插入條件檢查,主動釋放鎖允許寫操作:(此方法依賴線程調(diào)度器實現(xiàn),可能緩解但無法徹底避免饑餓)
void reader() {
while (true) {
{
std::shared_lock lock(rw_mutex);
// 讀操作...
}
std::this_thread::yield(); // 主動讓出CPU,增加寫者機(jī)會
}
}
六、總結(jié)
寫請求能否執(zhí)行:取決于鎖實現(xiàn)的公平性策略。
無公平性策略 → 可能饑餓(需業(yè)務(wù)層干預(yù))。
寫優(yōu)先或公平隊列 → 可避免饑餓。