1. 程式人生 > 其它 >雙重檢查鎖--聲名狼藉, 臭名昭著

雙重檢查鎖--聲名狼藉, 臭名昭著

雙重檢查鎖模式,是經常聽到和用到的方式,既保護了資料的初始化過程,也避免了每次訪問時,多個執行緒要序列化的檢查鎖問題。 不過,又有觀點說,雙重檢查鎖模式是聲名狼藉,是臭名昭著的。下面我們通過例子來分析論證。

直接貼程式碼,附上執行結果,我們先看效果,再做分析。
 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)的風險》 英文版