41. C++static线程安全与初始化顺序

静态变量的初始化线程安全问题

C++的局部static变量,是预先在静态存储区分配了内存,然后在第一次执行到这里的时候进行初始化。

C++11 规定了局部static变量的线程安全,实现上应该是类似std::call_once的实现,我估计基本上就是基于cas的spin-lock,这里当然可以根据编译器不同有不同的实现

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

static变量经常被用来实现单例模式。一个单例模式的类中都包含一个指向static类实例的指针,并提供一个公共接口返回这个指针以实现对这个实例的调用。因为在程序整个执行周期中,static变量只加载一次,保证了“仅此一个”的事实,如下代码所示:

class A {
public:
    static A* getInstance() {
        if (instance_ == NULL) 
        	instance_ = new A();
        return instance_;
    }
private:
	static A* instance_;
}
//static变量在类外定义
A* A::instance_ = NULL;

如上代码就实现了一个简单的C++单例模式。在单线程程序中,这个单例类没有任何问题,但是在多线程环境下,很容易就能发现方法getInstance中存在race condition。

class A {
public:
    static A* getInstance() {
        static A instance_;
        return &instance_;
    }

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

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

在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)

静态变量初始化顺序
初始化

对于已经初始化的全局和静态变量时存放在可执行文件的数据段(.data),对于未初始化的全局和静态变量,则在BSS段中。

从可执行程序的角度来说,如果一个数据未被初始化,就不需要为其分配空间,所以.data 和.bss 的区别就是 .bss 并不占用可执行文件的大小,仅仅记录需要用多少空间来存储这些未初始化的数据,而不分配实际空间,编译器往往通过memset(bss_str, len, 0)进行初始化。

.bss段什么时候会进行真正的初始化呢?记得一开始接触全局变量和静态变量的时候,书上就有提到,在可执行程序执行之前(main函数运行之前),会进行一些初始化操作,.bss就是在这个阶段进行初始化的。也就是说.data和.bss段的数据,在main()函数执行之前就初始化完成,那么,可以得出的结论是这部分数据不存在多线程竞争的问题(main()函数执行前还不存在多线程现象)。

初始化顺序

C++标准规定,在同一个编译单元中,对全局变量或者静态变量的初始化顺序与其定义顺序一致。但是对于不同的编译单元中的静态变量的初始化顺序,标准没有做规定,也就是说假如两个全局静态变量A和B分别存在与两个.cc文件中,那么编译器对于这俩的初始化顺序是不确定的。

解决

既然出现了因为不同编译单元中的静态变量初始化导致,那么就需要针对性的解决这个问题,通常有如下几个方案:

  • 将所有的静态全局变量放在一个编译单元中(如果涉及到依赖的话,需要修改顺序)。
  • 强制编译器在编译阶段进行初始化,通常有constexprconstinit两种。
  • Initialization On First Use,即在使用时候,通过函数获取静态对象的方式进行初始化。

你可能感兴趣的:(C++知识,c++,java,jvm)