双重检查锁--声名狼藉, 臭名昭著

双重检查锁模式,是经常听到和用到的方式,既保护了数据的初始化过程,也避免了每次访问时,多个线程要序列化的检查锁问题。 不过,又有观点说,双重检查锁模式是声名狼藉,是臭名昭著的。下面我们通过例子来分析论证。

直接贴代码,附上执行结果,我们先看效果,再做分析。
 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)的风险》 英文版

 

上一篇:使用delve调试golang


下一篇:谱分解(SD)