C++并发编程中static变量的问题

在C++中,static表示的是“静态初始化”,由其声明的变量因此也叫作“静态变量”,他们从完成初始化后就一直存在于程序运行空间中(确切地说位于静态变量区),直至程序退出或销毁。

如果按照变量的作用域来划分,静态变量可以分为3类:

  1. global variable,即全局变量
  2. static variable with file scope
  3. static variable with block scope

前两种不必多说,重点说一下第三种里的“block scope”,它可以表示类内部、函数内部,或者仅仅是由“{}”包括起来的一个code block。

同时,按照静态变量初始化的时机,初始化过程可分为:编译时初始化加载(运行)时初始化,前者主要发生在静态常量的编译过程中,如程序中“static int a = 3”因为此处3为常量,编译时就能确定,因此这里就发生的是编译时初始化,反之,如果不是编译时初始化,那就必定进行的是加载时初始化。

static变量经常被用来实现单例模式。在设计模式中,单例模式广为人知,即使没有系统学习过设计模式的人很可能也对单例模式不会陌生。设计实现一个单例模式并非难事,但在多线程并发环境下这个问题就貌似不那么简单了。
通常来说,一个单例模式的类中都包含一个指向static类实例的指针,并提供一个公共接口返回这个指针以实现对这个实例的调用。因为在程序整个执行周期中,static变量只加载一次,保证了“仅此一个”的事实,如下代码所示:

#1    class A {
#2    public:
#3        static A* getInstance() {
#4            if (instance_ == NULL) 
#5                instance_ = new A();
#6            return instance_;
#7        }
#8    private:
#9        static A* instance_;
#10    }
#11    //static变量在类外定义
#12    A* A::instance_ = NULL;

如上代码就实现了一个简单的C++单例模式。在单线程程序中,这个单例类没有任何问题,但是在多线程环境下,很容易就能发现方法getInstance中存在race condition,如果线程A运行到了#4,此时instance_为NULL,这时线程B也运行到了这里,instance_此时还未进行构造,它的值还是NULL,那么接下来两个线程将分别执行new A()的操作,本为单例的类实际上并非单例。为了克服这个race condition,人们尝试用加锁、双阶段加锁等方法来解决它,虽然可以实现线程安全性,但毕竟增加了代码量以及复杂性,性能也受到了一定的影响。因此又有了另一种方法来实现单例类,如下代码所示:

#1    class A {
#2    public:
#3        static A* getInstance() {
#4            static A instance_;
#5            return &instance_;
#6        }
#7    }

根据static的语义,其只在程序对类A加载时进行一次初始化,全局只有这一个实例。看起来很美好,而且代码更少了。但是这实际上是不对的,对于在编译时进行初始化的static变量,它一定是线程安全的,但是对于这种加载时进行初始化的变量,编译器生成的代码实际上类似这样:

static bool initialized = false;
static A instance_;
if (initialized == false) {
    initialized = true;
    instance_ = A();
}
return &instance_;

看到了吗?实际上还是会有race condition。那么如何在不加锁、不引入复杂设计的情况下实现单例模式呢?
还是要感谢C++的发展,上述代码是在C++11以前的标准中得到的,而在C++11标准中,static的语义实际上已经是线程安全的了。引用C++11标准中的内容:

If control enters the declaration concurrently while the variable is being initialized,the concurrent execution shall wait for completion of the initialization.

在网站cppreference中,也有类似的描述:

If multiple threads attempt to initialize the same static local variable concurrently,the initialization occurs exactly once(since c++11)

因此,我认为在C++11中,以上代码是可以实现一个线程安全的单例类的。

点赞