雙重檢查鎖--聲名狼藉, 臭名昭著
阿新 • • 發佈:2022-01-17
雙重檢查鎖模式,是經常聽到和用到的方式,既保護了資料的初始化過程,也避免了每次訪問時,多個執行緒要序列化的檢查鎖問題。 不過,又有觀點說,雙重檢查鎖模式是聲名狼藉,是臭名昭著的。下面我們通過例子來分析論證。
直接貼程式碼,附上執行結果,我們先看效果,再做分析。
1 xxx.h 2 ---------------------------- 3 #include <iostream> 4 #include <mutex> 5 #include <thread> 6 #include <chrono> 7 8 9 //! [0] C風格:面向過程的雙重檢查鎖10 //share data 11 struct Share_Data{ 12 int sd_i; 13 double sd_d; 14 char sd_c; 15 16 std::mutex prt_mtx; 17 void printVal(){ 18 19 std::lock_guard<std::mutex> lkgd(prt_mtx); 20 std::cout<<"sd_i:"<<sd_i<<std::endl; 21 std::cout<<"sd_d:"<<sd_d<<std::endl; 22 std::cout<<"sd_c:"<<sd_c<<std::endl; 23 std::cout<<"--------------"<<std::endl; 24 } 25 }; 26 27 extern Share_Data * g_sd_var; 28 extern std::mutex g_mtx; 29 extern void thread_fun(); 30 //! [0]
1 xxx.cpp 2 -------------------- 3#include "Double_Checked_Lock.h" 4 5 Share_Data * g_sd_var = nullptr; 6 std::mutex g_mtx; 7 8 void thread_fun(){ 9 if (!g_sd_var){ 10 std::lock_guard<std::mutex> lkgd(g_mtx); 11 if (!g_sd_var){ 12 g_sd_var = new Share_Data; 13 14 //模擬耗時的資源初始化 15 std::chrono::milliseconds sleep_time(500); 16 std::this_thread::sleep_for(sleep_time); 17 g_sd_var->sd_i = 100; 18 std::this_thread::sleep_for(sleep_time); 19 g_sd_var->sd_d = 200.2; 20 std::this_thread::sleep_for(sleep_time); 21 g_sd_var->sd_c = 'A'; 22 } 23 } 24 g_sd_var->printVal(); //後續僅讀取訪問 25 }
1 main.cpp 2 ------------------------------- 3 #include "Double_Checked_Lock.h" 4 int main(int argc, char *argv[]) 5 { 6 QCoreApplication a(argc, argv); 7 8 std::chrono::milliseconds sleep_time(300); 9 std::thread th_a(thread_fun); 10 std::this_thread::sleep_for(sleep_time); 11 12 std::thread th_b(thread_fun); 13 std::this_thread::sleep_for(sleep_time); 14 15 std::thread th_c(thread_fun); 16 std::this_thread::sleep_for(sleep_time); 17 18 std::thread th_d(thread_fun); 19 std::this_thread::sleep_for(sleep_time); 20 21 std::thread th_e(thread_fun); 22 std::this_thread::sleep_for(sleep_time); 23 24 th_a.join(); 25 th_b.join(); 26 th_c.join(); 27 th_d.join(); 28 th_e.join(); 29 return a.exec(); 30 }
1 執行輸出的結果如下: 2 ------------------------------ 3 sd_i:-842150451 4 sd_d:-6.27744e+66 5 sd_c: 6 -------------- 7 sd_i:100 8 sd_d:-6.27744e+66 9 sd_c: 10 -------------- 11 sd_i:100 12 sd_d:-6.27744e+66 13 sd_c: 14 -------------- 15 sd_i:100 16 sd_d:200.2 17 sd_c: 18 -------------- 19 sd_i:100 20 sd_d:200.2 21 sd_c:A 22 --------------
總結:驚不驚喜,意不意外,哈哈哈。想要的結果是每個執行緒都輸出100;200.2;A;實際上卻不是。以後不要使用“雙重檢查鎖模式”咯,它是臭名昭著的!
下面我們來分析一下,錯哪裡了,導致雙重鎖檢查聲名狼藉。
這個模式為什麼宣告狼藉呢? 因為這裡存在潛在的條件競爭。未被鎖保護的讀取操作(第一次檢查)沒有與其他執行緒裡被鎖保護的寫入操作(第二次檢查後的初始化過程)進行同步,因此就會產生條件競爭。
這個條件競爭不僅覆蓋指標本身,還會影響到其指向的物件; 即使一個執行緒知道另一個執行緒完成對指標進行寫入,它可能沒有看到新建立的物件例項,然後呼叫讀取操作介面,就會得到不正確的結果。
這個例子是一種典型的條件競爭-----資料競爭,C++標準中這會被指定為 “未定義行為” 。可以參考,著名的《C++和雙重檢查鎖定模式(DCLP)的風險》 英文版。