C++11保护共享数据的其他方法

保护共享数据的初始化过程

在多线程编程中,互斥量是最通用的保护共享数据的机制。但是在某些情况下,一些资源仅需要在第一次初始化的时候需要保护,其时候就可以不需要互斥变量的保护了。比如编码中最常见的单例模式,核心代码如下:

//(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

猜你喜欢

转载自blog.csdn.net/danshiming/article/details/114208242