C++线程安全的数据初始化 -- 最优雅的单例?

前言

如果在线程之间共享的数据不需要更改时,共享数据只需要以线程安全的方式初始化就行了。这样每次访问数据时就没有必要使用花费较大代价的锁了。

C ++中有三种方法以线程安全的方式初始化变量:
- 1、常量表达式
- 2、函数std::call_once与标志std::once_flag的结合使用
- 3、具有块范围的静态变量


常量表达式

常量表达式是编译器在编译期间可以初始化的表达式。所以,它们是隐式线程安全的。通过在表达式类型前面使用关键字constexpr使其成为常量表达式:
constexpr double pi = 3.14;
此外,用户定义的类型也可以是常量表达式。对于这些类型,在编译时初始化它们有一些限制:

1、它们不能具有虚方法或虚基类。
2、它们的构造函数必须是空的,并且本身是一个常量表达式。
3、它们的方法可以在编译时调用,必须是常量表达式。

下面的结构体MyDouble满足所有这些要求。因此,可以在编译时实例化MyDouble的对象。此实例化是线程安全的:

struct MyDouble{
  constexpr MyDouble(double v): val(v){}
  constexpr double getValue(){ return val; }
private:
  double val;
};

constexpr MyDouble myDouble(10.5);
std::cout << myDouble.getValue() << std::endl;

函数call_once与once_flag结合使用

通过使用std::call_once函数,你可以注册给所有可调用的函数、lambda等(callable)。
std::once_flag这个东西保证,只有一个注册的功能将被调用。
因此,虽然您可以通过once_flag注册更多不同的函数,但是只会有一个函数被调用。
下面这个程序展示了call_once与once_flag的结合使用:

// callOnce.cpp

#include <iostream>
#include <thread>
#include <mutex>

std::once_flag onceFlag;

void do_once(){
  std::call_once(onceFlag, [](){ std::cout << "Only once." << std::endl; });
}

int main(){

  std::cout << std::endl;

  std::thread t1(do_once);
  std::thread t2(do_once);
  std::thread t3(do_once);
  std::thread t4(do_once);

  t1.join();
  t2.join();
  t3.join();
  t4.join();

  std::cout << std::endl;

}

程序启动4个线程(第17-20行),它们每个都尝试调用do_once函数。预期的结果是字符串“Only once.”仅显示一次。
这里写图片描述


著名的单例模式需要保证只创建一个对象实例。
在多线程环境中,这是一项具有挑战性的任务。
但是,有了std::call_once和std::once_flag这个就很容易做到了。
现在,以线程安全的方式初始化实例:

// singletonCallOnce.cpp

#include <iostream>
#include <mutex>

class MySingleton{

  private:
    static std::once_flag initInstanceFlag;
    static MySingleton* instance;
    MySingleton()= default;
    ~MySingleton()= default;

  public:
    MySingleton(const MySingleton&)= delete;
    MySingleton& operator=(const MySingleton&)= delete;

    static MySingleton* getInstance(){
      std::call_once(initInstanceFlag,MySingleton::initSingleton);
      return instance;
    }

    static void initSingleton(){
      instance= new MySingleton();
    }
};

MySingleton* MySingleton::instance= nullptr;
std::once_flag MySingleton::initInstanceFlag;


int main(){

  std::cout << std::endl;

  std::cout << "MySingleton::getInstance(): "<< MySingleton::getInstance() << std::endl;
  std::cout << "MySingleton::getInstance(): "<< MySingleton::getInstance() << std::endl;

  std::cout << std::endl;

}

首先在第9行声明静态成员变量std::once_flag,在第29行进行初始化。
静态方法getInstance(第28-21行)使用该标志initInstanceFlag来确保静态方法initSingleton(第23-25行)只执行一次,在这个方法的主体中,创建单例。

打印结果正如所料,显示的是MySingleton::getIstance()方法获取的单例对象的的地址:
这里写图片描述


具有块范围的静态变量

具有块作用域的静态变量将只创建一次。
这个特征是所谓的Meyers Singleton的基础,以Scott Meyers的名字来命名,这是迄今为止单例模式最优雅的实现:

#include <thread>

class MySingleton{
public:
  static MySingleton& getInstance(){
    static MySingleton instance;
    return instance;
  }
private:
  MySingleton();
  ~MySingleton();
  MySingleton(const MySingleton&)= delete;
  MySingleton& operator=(const MySingleton&)= delete;

};

MySingleton::MySingleton()= default;
MySingleton::~MySingleton()= default;

int main(){

  MySingleton::getInstance();

}

通过使用关键字default,您可以从编译器请求特殊方法,它们很特殊,因为只有编译器才能创建它们。
使用delete关键字则不会创建编译器自动生成的相应的方法,因此无法调用。
如果您尝试使用它们,您将收到编译时错误。
Meyers Singleton在多线程程序中的意义?Meyers Singleton是线程安全的。


旁注:双重检查锁的单例模式

现在有一种错误的信念存在,认为在多线程环境中线程安全初始化的另一种方式是双重检查锁的单例模式。
双重检查锁的模式一般来说是一种不安全的初始化单例的方式。
它经常假设是各种语言的经典实现方案,但是不管Java、C或C++内存模型没有给出这种保证。它主要是假设了单例的访问是原子的。


但是,双重检查锁的模式是什么?
在线程安全的方式中实现单例模式的第一个想法是通过锁来保护单例的初始化:

mutex myMutex;

class MySingleton{
public:  
  static MySingleton& getInstance(){    
    lock_guard<mutex> myLock(myMutex);      
    if( !instance ) instance= new MySingleton();    
    return *instance;  
  }
private:  
  MySingleton();  
  ~MySingleton();  
  MySingleton(const MySingleton&)= delete;  
  MySingleton& operator=(const MySingleton&)= delete;
  static MySingleton* instance;
};
MySingleton::MySingleton()= default;
MySingleton::~MySingleton()= default;
MySingleton* MySingleton::instance= nullptr;

有什么问题?没有。
该实现是线程安全的。但是性能损失很大!!!
第6行中单例的每次访问都受到锁的保护,不管是读还是写操作,大部分时间都没有必要。


我们用所谓的双重检查锁定模式来拯救下这种方式吧:

static MySingleton& getInstance(){    
  if ( !instance ){      
    lock_guard<mutex> myLock(myMutex);      
    if( !instance ) instance= new MySingleton();    
  }  
  return *instance; 
}

我在第2行使用代价很小的指针比较而不是代价高的锁。
只有当我得到一个空指针时,我才在单例上应用高代价的锁(第3行)。
因为有可能另一个线程将初始化指针比较(第2行)和锁定(第3行)之间的单例,我必须在第4行执行额外的指针比较。
所以顾名思义,检查两次,只锁一次。


是不是很机智?看起来还真是。
但是线程安全吗?不安全。。。
哪儿有问题?第4行中的调用 instance = new MySingleton()至少包含三个步骤:

1、为MySingleton分配内存
2、在内存中创建MySingleton对象
3、让实例引用MySingleton对象

问题是:无法保证这些步骤的顺序!!!
例如,出于优化原因,处理器可以将步骤重新排序到序列1->3->2。
这样,在第一步骤中将分配内存,而在第二步骤中,实例极有可能是个不完整的实例(还没初始化完成)。如果在那个时候另一个线程试图访问单例,它会比较指针并得到答案为真。所以,另一个线程有一种错觉,即它以为它正在处理一个完整的单例。

结果很简单:程序行为未定义。


原文地址:

http://www.modernescpp.com/index.php/thread-safe-initialization-of-data
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值