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中,以上代码是可以实现一个线程安全的单例类的。

相关文章
|
2月前
|
存储 编译器 C语言
详解C/C++中的static和extern
本文详解了C/C++中`static`和`extern`关键字的用法和区别,通过具体代码示例说明了在不同情境下如何正确使用这两个关键字,以及`extern "C"`在C++中用于兼容C语言库的特殊作用。
详解C/C++中的static和extern
|
1月前
|
存储 编译器 C++
【C++】深入探索类和对象:初始化列表及其static成员与友元(一)
【C++】深入探索类和对象:初始化列表及其static成员与友元
|
1月前
|
C语言 C++
C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
在C语言中,`static`关键字主要用于变量声明,使得该变量的作用域被限制在其被声明的函数内部,且在整个程序运行期间保留其值。而在C++中,除了继承了C的特性外,`static`还可以用于类成员,使该成员被所有类实例共享,同时在类外进行初始化。这使得C++中的`static`具有更广泛的应用场景,不仅限于控制变量的作用域和生存期。
53 10
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解2
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解
29 3
|
1月前
|
编译器 C++
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解1
【C++篇】C++类与对象深度解析(四):初始化列表、类型转换与static成员详解
45 3
|
1月前
|
C++
【C++】深入探索类和对象:初始化列表及其static成员与友元(二)
【C++】深入探索类和对象:初始化列表及其static成员与友元
|
1月前
|
编译器 C++
【C++】深入探索类和对象:初始化列表及其static成员与友元(三)
【C++】深入探索类和对象:初始化列表及其static成员与友元
|
1月前
|
C语言 C++
实现两个变量值的互换[C语言和C++的区别]
实现两个变量值的互换[C语言和C++的区别]
19 0
|
3月前
|
存储 安全 C++
C++:指针引用普通变量适用场景
指针和引用都是C++提供的强大工具,它们在不同的场景下发挥着不可或缺的作用。了解两者的特点及适用场景,可以帮助开发者编写出更加高效、可读性更强的代码。在实际开发中,合理选择使用指针或引用是提高编程技巧的关键。
33 1
|
2月前
|
JavaScript 前端开发 Java
通过Gtest访问C++静态、私有、保护变量和方法
通过Gtest访问C++静态、私有、保护变量和方法
68 0