std::once_flag结构体和std::call_once()函数搭配来处理条件竞争
C++标准委员会也认为条件竞争的处理很重要。所以,C++标准库提供了 std::once_flag结构体 和 std::call_once() 函数,来处理条件竞争(这种情况的条件竞争:臭名昭著的双重检查锁模式)。
比起锁住互斥量并显示的检查指针,只需要使用 std::call_once() 就可以, 在 std::call_once()函数执行结束时,就能安全的知道指针已经被安全的初始化了。
使用 std::call_once() 会比显示的使用互斥量消耗的资源更少, 特别是当初始化完成之后。
下面我们一起看一个使用例子:
1 xxx.h 2 -------------------------- 3 4 #include <iostream> 5 #include <mutex> 6 #include <thread> 7 #include <chrono> 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 "NewStd_Call_Once.h" 4 5 Share_Data * g_sd_var = nullptr; 6 std::mutex g_mtx; 7 std::once_flag init_Flag; 8 void initShareData(){ 9 if (!g_sd_var){ 10 g_sd_var = new Share_Data; 11 12 //模拟耗时的资源初始化 13 std::chrono::milliseconds sleep_time(500); 14 std::this_thread::sleep_for(sleep_time); 15 g_sd_var->sd_i = 100; 16 std::this_thread::sleep_for(sleep_time); 17 g_sd_var->sd_d = 200.2; 18 std::this_thread::sleep_for(sleep_time); 19 g_sd_var->sd_c = 'A'; 20 } 21 } 22 void thread_fun(){ 23 std::call_once(init_Flag, initShareData); 24 g_sd_var->printVal(); //后续仅读取访问 25 }
1 main.cpp 2 --------------------- 3 #include "NewStd_Call_Once.h" 4 5 int main(int argc, char *argv[]) 6 { 7 QCoreApplication a(argc, argv); 8 9 //! [1] std::call_once 好用,经典~! 10 std::chrono::milliseconds sleep_time(300); 11 std::thread th_a(thread_fun); 12 std::this_thread::sleep_for(sleep_time); 13 14 std::thread th_b(thread_fun); 15 std::this_thread::sleep_for(sleep_time); 16 17 std::thread th_c(thread_fun); 18 std::this_thread::sleep_for(sleep_time); 19 20 std::thread th_d(thread_fun); 21 std::this_thread::sleep_for(sleep_time); 22 23 std::thread th_e(thread_fun); 24 std::this_thread::sleep_for(sleep_time); 25 26 th_a.join(); 27 th_b.join(); 28 th_c.join(); 29 th_d.join(); 30 th_e.join(); 31 //! [1] 32 return a.exec(); 33 }
1 执行输出的结果如下: 2 ------------------------------ 3 sd_i:100 4 sd_d:200.2 5 sd_c:A 6 -------------- 7 sd_i:100 8 sd_d:200.2 9 sd_c:A 10 -------------- 11 sd_i:100 12 sd_d:200.2 13 sd_c:A 14 -------------- 15 sd_i:100 16 sd_d:200.2 17 sd_c:A 18 -------------- 19 sd_i:100 20 sd_d:200.2 21 sd_c:A 22 --------------
总结:std::call_flag结构体 和 std::call_once()函数,用起来太有爱了,非常棒!
值得注意的是,std::once_flag 和 std::mutex 一样,不可以拷贝和移动。
还有一种初始化过程中潜存着条件竞争:变量被声明为 static 类型, 这种变量在声明后就已经完成初始化; 对于多线程环境中,这就意味着这里有条件竞争---抢着去定义这个变量。
在很多不支持C++标准的编译器上,在实践中,这样的条件竞争是确实存在的,因为在多线程中,每个线程都认为他们是第一个初始化这个变量的线程;
或一个线程对变量进行初始化,而另外一个线程要使用这个变量时,初始化过程还没完成。
在C++11标准中,这些问题都被解决了:初始化及定义完全在一个线程中发生,并且没有其他线程可在初始化完成前对其进行处理,条件竞争终止于初始化阶段,这样比在之后再去处理好的多。
那么,基于此,在只需要一个全局实例情况下,这里提供一个 std::call_once() 的替代方案。
1 class myclass; 2 myclass& get_my_class_instance(){ 3 static myclass instance; 4 return instance; 5 }
C++11标准中,多线程可以安全的调用 get_my_class_instance()函数,而不用为数据竞争而担心。
还有一种场景,我们称之为:std::call_once() 作为类的成员的延迟初始化。