C++中多线程与Singleton的那些事儿

发表于:2015-3-11 10:08

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:origins    来源:51Testing软件测试网采编

分享:
  DCL
  DCL即double-checked locking。在普通加锁的写法中,每次调用getInstance都会进入临界区,这样在heavy contention的情况下该函数就会成为系统性能的瓶颈,这个时候就有先驱者们想到了DCL写法,也就是进行两次check,当第一次check为假时,才加锁进行第二次check:
template<typename T>
class Singleton
{
public:
static T& getInstance()
{
if(!value_)
{
MutexGuard guard(mutex_);
if (!value_)
{
value_ = new T();
}
}
return *value_;
}
private:
Singleton();
~Singleton();
static T*     value_;
static Mutex  mutex_;
};
template<typename T>
T* Singleton<T>::value_ = NULL;
template<typename T>
Mutex Singleton<T>::mutex_;
  是不是觉得这样就完美啦?其实在一段时间内,大家都以为这是正确的、有效的做法。实际上却不是这样的。幸运的是,后来有大牛们发现了DCL中的问题,避免了这样错误的写法在更多的程序代码中出现。
  那么到底错在哪呢?我们先看看第12行value_ = new T这一句发生了什么:
  分配了一个T类型对象所需要的内存。
  在分配的内存处构造T类型的对象。
  把分配的内存的地址赋给指针value_
  主观上,我们会觉得计算机在会按照1、2、3的步骤来执行代码,但是问题就出在这。实际上只能确定步骤1最先执行,而步骤2、3的执行顺序却是不一定的。假如某一个线程A在调用getInstance的时候第12行的语句按照1、3、2的步骤执行,那么当刚刚执行完步骤3的时候发生线程切换,计算机开始执行另外一个线程B。因为第一次check没有上锁保护,那么在线程B中调用getInstance的时候,不会在第一次check上等待,而是执行这一句,那么此时value_已经被赋值了,就会直接返回*value_然后执行后面使用T类型对象的语句,但是在A线程中步骤3还没有执行!也就是说在B线程中通过getInstance返回的对象还没有被构造就被拿去使用了!这样就会发生一些难以debug的灾难问题。
  volatile关键字也不会影响执行顺序的不确定性。
  在多核心机器的环境下,2个核心同时执行上面的A、B两个线程时,由于第一次check没有锁保护,依然会出现使用实际没有被构造的对象的情况。
  关于DCL问题的详细讨论分析,可以参考Scott Meyer的paper:《C++ and the Perils of Double-Checked Locking》
  不过在新的C++11中,这个问题得到了解决。因为新的C++11规定了新的内存模型,保证了执行上述3个步骤的时候不会发生线程切换,相当这个初始化过程是“原子性”的的操作,DCL又可以正确使用了,不过在C++11下却有更简洁的多线程Singleton写法了,这个留在后面再介绍。
  关于新的C++11的内存模型,可以参考:C++11中文版FAQ:内存模型、C++11FAQ:Memory Model、C++ Data-Dependency Ordering: Atomics and Memory Model
  可能有人要问了,那么有什么办法可以在C++11之前的版本下,使得DCL正确工作呢?要使其正确执行的话,就得在步骤2、3直接加上一道memory barrier。强迫CPU执行的时候按照1、2、3的步骤来运行。(经网友@shines77提醒,因没有锁的缘故这里需要用RCU技法,即read-copy-update)
  static T& getInstance()
  {
  if(!value_)
  {
  MutexGuard guard(mutex_);
  if (!value_)
  {
  T* p = static_cast<T*>(operator new(sizeof(T)));
  new (p) T();
  // insert some memory barier
  value_ = p;  // RCU method
  }
  }
  return *value_;
  }
  也许有人会说,你这已经把先前的value_ = new T()这一句拆成了下面这样的两条语句, 为什么还要在后面插入some memory barrier?
  T* p = static_cast<T*>(operator new(sizeof(T)));
  new (p) T();
  原因是现代处理器都是以Out-of-order execution(乱序执行)的方式来执行指令的。现代CPU基本都是多核心的,一个核包含多个执行单元。例如,一个现代的Intel CPU 包含6个执行单元,可以做一组数学,条件逻辑和内存操作的组合。每个执行单元可以做这些任务的组合。这些执行单元并行地操作,允许指令并行地执行。如果从其它 CPU 来观察,这引入了程序顺序的另一层不确定性。
  如果站在单个CPU核心的角度上讲,它(一个CPU核心)看到的程序代码都是单线程的,所以它在内部以自己的“优化方式”乱序、并行的执行代码,然后保证最终的结果和按代码逻辑顺序执行的结果一致。但是如果我们编写的代码是多线程的,当不同线程访问、操作共享内存区域的时候,就会出现CPU实际执行的结果和代码逻辑所期望的结果不一致的情况。这是因为以单个CPU核心的视角来看代码是“单线程”的。
  所以为了解决这个问题,就需要memory barrier了,利用它来强迫CPU按代码的逻辑顺序执行。例如上面改动版本的getInstance代码中,因为第10行有memory barrier,所以CPU执行第9、10、11按“顺序”执行的。即使在CPU核心内是并行执行指令(比如一个单元执行第9行、一个单元执行第11行)的,但是他们在退役单元(retirement unit)更新执行结果到通用寄存器或者内存中时也是按照9、10、11顺序更新的。例如一个单元A先执行完了第11行,CPU让单元A等待直到执行第9行的单元B执行完成并在退役单元更新完结果以后再在退役单元更新A的结果。
  memory barreir是一种特殊的处理器指令,他指挥处理器做下面三件事:(参考文章Mutex And Memory Visibility)
  刷新store buffer。
  等待直到memory barreir之前的操作已经完成。
  不将memory barreir之后的操作移到memory barreir之前执行。
  通过使用memory barreir,可以确保之前的乱序执行已经全部完成,并且未完成的写操作已全部刷新到主存。因此,数据一致性又重新回到其他线程的身边,从而保证正确内存的可见性。实际上,原子操作以及通过原子操作实现的模型(例如一些锁之类的),都是通过在底层加入memory barrier来实现的。
  至于如何加入memory barrier,在unix上可以通过内核提供的barrier()宏来实现。或者直接嵌入ASM汇编指令mfence也可以,barrier宏也是通过该指令实现的。
  关于memory barreir可以参考文章Memory Barriers/Fences。
  Meyers Singleton
  Scott Meyer在《Effective C++》中提出了一种简洁的singleton写法
  template<typename T>
  class Singleton
  {
  public:
  static T& getInstance()
  {
  static T value;
  return value;
  }
  private:
  Singleton();
  ~Singleton();
  };
  先说结论:
  单线程下,正确。
  C++11及以后的版本(如C++14)的多线程下,正确。
  C++11之前的多线程下,不一定正确。
  原因在于在C++11之前的标准中并没有规定local static变量的内存模型,所以很多编译器在实现local static变量的时候仅仅是进行了一次check(参考《深入探索C++对象模型》),于是getInstance函数被编译器改写成这样了:
  bool initialized = false;
  char value[sizeof(T)];
  T& getInstance()
  {
  if (!initialized)
  {
  initialized = true;
  new (value) T();
  }
  return *(reinterpret_cast<T*>(value));
  }
  于是乎它就是不是线程安全的了。
42/4<1234>
100家互联网大公司java笔试题汇总,填问卷领取~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2023
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号