双重检查锁--声名狼藉, 臭名昭著
双重检查锁模式,是经常听到和用到的方式,既保护了数据的初始化过程,也避免了每次访问时,多个线程要序列化的检查锁问题。 不过,又有观点说,双重检查锁模式是声名狼藉,是臭名昭著的。下面我们通过例子来分析论证。
直接贴代码,附上执行结果,我们先看效果,再做分析。
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)的风险》 英文版。