保护共享数据的初始化过程
在多线程编程中,互斥量是最通用的保护共享数据的机制。但是在某些情况下,一些资源仅需要在第一次初始化的时候需要保护,其时候就可以不需要互斥变量的保护了。比如编码中最常见的单例模式,核心代码如下:
//(3)获得本类实例的唯一全局访问点
static CSinglton* GetInstance()
{
//若实例不存在,则创建实例对象
if (NULL == pInstance)
{
pInstance = new CSinglton();
}
//实例已经存在,直接该实例对象
return pInstance;
}
以上代码在多线程环境中是不安全的,有可能导致创建对象两次,导致内存泄漏;如果每次访问之前都是加锁,将大大影响访问性能,即使能解决问题。
为了满足多线程环境中高效和安全的特性,人们提出了双重锁定方式单例模式,代码如下:
static CSinglton* GetInstance()
{
//仅在实例未被创建时加锁,其他时候直接返回
if (NULL == pInstance)
{
std::lock_guard<std::mutex> guard(g_mutex)
if (NULL == pInstance)
{
//若实例不存在,则创建实例对象
pInstance = new CSinglton();//步骤一
}
}
//实例已经存在,直接该实例对象
return pInstance;
}
//执行代码
GetInstance()->dosomething();//步骤二
但是在实践中可以发现,会因为指令编排方式不同,导致一些异常情况发生。
因为线程A中pInstance分配了指针,CSinglton的构造函数还没有执行,但此时线程B开始调用GetInstance()接口,因为pInstance已经非空,直接执行dosomething(),就导致异常情况发生了。
同样的,创建静态局部变量的代码,在C++11之前也存在抢着创建变量的问题:
class my_class;
my_class& get_my_class_instance()
{
static my_class instance; // 多个线程同时创建实例。
return instance;
}
std::call_once保护共享数据
为了解决双重锁定以及创建多个实例的问题,C++11标准库提供了std::once_flag和std::call_once来处理只初始化一次的情况。使用std::call_once比显式使用互斥量消耗的资源更少,并且还是无锁式编程,减少了死锁问题的发生。
call_once和once_flag的用法:
std::once_flag flag;
void simple_do_once()
{
std::call_once(flag, [](){ std::cout << "Simple example: called once\n"; });
}
int _tmain(int argc, _TCHAR* argv[])
{
std::thread st1(simple_do_once);
std::thread st2(simple_do_once);
std::thread st3(simple_do_once);
std::thread st4(simple_do_once);
st1.join();
st2.join();
st3.join();
st4.join();
std::cout << "main thread end\n";
}
运行结果:
Simple example: called once//只有一个打印,目标函数仅被执行一次
main thread end
如何用std::call_once创建安全性的单例模式参见这篇文章call_once 使用方法
发现问题
在正常情况下,若在执行期间call_once调用的函数抛出异常,则once_flag状态不会翻转,其他线程还可以继续执行call_once;但是在vs2013测试发现,异常无法安装预期进行,不知道是何种原因,这里做个记录。
测试代码:
std::once_flag flag;
void may_throw_function(bool do_throw)
{
if (do_throw)
{
std::cout << "throw: call_once will retry\n"; // 这会出现多于一次
throw std::exception();
}
std::cout << "Didn't throw, call_once will not attempt again\n"; // 保证一次
}
void do_once(bool do_throw)
{
try
{
std::call_once(flag, may_throw_function, do_throw);
}
catch (...)
{
}
}
int main(int argc, _TCHAR* argv[])
{
std::thread t1(do_once, true);
std::thread t2(do_once, true);
std::thread t3(do_once, false);
std::thread t4(do_once, true);
t1.join();
t2.join();
t3.join();
t4.join();
std::cout << "main thread end\n";
}
运行结果:
vs2013编译的程序无法继续执行,线程卡住,如下
td::call_once的替代方案
在前面的文章中提到,我们也可以使用静态局部变量的方式创建唯一实例,只是在多线程环境中,存在这么一种情况:每个线程都认为他们是第一个初始化这个变量,导致这个变量被创建两次。
但在C++11标准中,这些问题都被解决了:初始化及定义完全在一个线程中发生,并且没有其他线程可在初始化完成前对其进行处理,条件竞争终止于初始化阶段。当然,这个要求编译要支持C++11才可以。
std::call_once的替代方案
class my_class;
my_class& get_my_class_instance()
{
static my_class instance; // 线程安全的初始化过程
return instance;
}
本文转自:https://blog.csdn.net/c_base_jin/article/details/89603994