C++单例模式与线程安全

C++单例模式与线程安全

最简单的单例模式可以是

// single thread safe version
class Singleton {
    public:
        static Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                instance_ = new Singleton(x);
            }
            return instance_;
        }
        void Print() {
            std::cout << this->member_ << std::endl;
        }
    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

然而,在多线程情况下,比如线程A 判断instance_ 为空后,此时线程A暂停执行挂起,而线程B 判断 instance_ 也为空,于是会调用构造函数创建对象,并返回,而线程A继续恢复运行后,同样会调用构造函数创建对象,并修改 instance_ 指针。导致第一个指针被修改,引发线程安全问题。

基于此,可以在判断 instance_ 为空前,先加锁,拿到锁的线程才能进一步判断 instance_ 是否为空,并进一步决定是否创建该对象。代码如下

// thread safe, but inefficiency
class Singleton {
    public :
        static Singleton* GetInstance(int x = 0) {
            std::lock_guard<std::mutex> lock(mtx);
            if (instance_ == NULL) {
                instance_ = new Singleton(x);
            }
            return instance_;
        }
        void Print() {
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable    
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

然而,此方法存在效率问题,每次调用 GetInstance()获取该对象指针时,都需要加锁。显然,我们只是希望在首次创建对象的时候才需要加锁,后续的访问不用再加锁(已经创建好了对象),于是很自然的想到在加锁前,再进行一次判断 instance_ 是否为空,代码如下

// double-checked locking
class Singleton {
    public :
        static Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                std::lock_guard<std::mutex> lock(mtx);
                if (instance_ == NULL) {
                    instance_ = new Singleton(x);
                    /*
                    instance_ =       //  point to memory
                        operator new(sizeof(Singleton));  // memory allocate
                    new (instance_) Singleton;  //  construct a object int the allocated memory, may occur exception
                    */
                }
            }
            return instance_b_;
        }
        void Print() { 
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable    
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

上述方法很好的解决了频繁加锁带来的开销,但是仍然存在一些问题,对于 new 调用,其过程为

  1. 从堆区分配内存
  2. 在分配的内存执行构造函数
  3. 返回其指针

而实际上,经过编译器优化后的代码执行顺序可能并不是这样的,比如

instance_ = new Singleton(x);

可能等价于

instance_ = operator new(sizeof(Singleton));  // memory allocate
new (instance_) Singleton;  // placement new

  1. 从堆区分配内存
  2. 返回堆区的指针
  3. 对指针指向的内存执行构造函数

该情形下,如果线程A在执行到 instance_ = operator new(sizeof(Singleton)); 后,暂停运行挂起,线程B在进入GetInstance() 并判断 instance_ 是否为空时,发现其不为空,于是返回 instance_ 指针,然而该指针是没有经过初始化的,所以线程B对该指针的使用将可能会引起未定义的行为。为了防止发生编译器的优化后的代码执行顺序和我们预期的不一致(上述2,3步调换了顺序),我们希望确保在 new 调用构造函数成功后,再修改 instance_ 指针,代码如下

// in-case exception occur when construct
class Singleton {
    public :
        static Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                std::lock_guard<std::mutex> lock(mtx);
                if (instance_ == NULL) {
                    Singleton* temp = new Singleton(x);
                    instance_ = temp;
                }
            }
            return instance_;
        }
        void Print() { 
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable    
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

这里我们引入了 temp 指针,用于确保 new 调用成功后,再去修改 instance_ 指针,对new 进行替换后如下

// in-case exception occur when construct
class Singleton {
    public :
        static Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                std::lock_guard<std::mutex> lock(mtx);
                if (instance_ == NULL) {
                    Singleton* temp = operator new(sizeof(Singleton));  // memory allocate
										new (temp) Singleton;  // placement new
                    instance_ = temp;
                }
            }
            return instance_;
        }
        void Print() { 
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable    
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

然而,对于上述代码,编译器仍可能对进行优化,编译器会发现temp 变量在程序中只是起到一个传递值的作用,可以被优化掉,优化后的代码将直接是 instance_ = new Singleton(x); ,为了防止编译器出现此类优化,我们可以进一步使用 volatile 关键字来防止编译器的优化,代码如下

class Singleton {
    public :
        static Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                std::lock_guard<std::mutex> lock(mtx);
                if (instance_ == NULL) {
                    Singleton* volatile temp = new Singleton(x);
                    instance_ = temp;
                }
            }
            return instance_;
        }
        void Print() { 
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable    
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

上述代码对temp 变量声明为 volatile,其目的在于告诉编译器,temp相关的代码块不能进行优化,temp相关的指令序列和高级语言看到的应该完全一致。

然而,这里还是可能存在问题,volatile只保证了对temp变量的相关代码的操作顺序,而对temp的成员没有保证,比如下述代码

// in-case exception occur when construct
class Singleton {
    public :
        static Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                std::lock_guard<std::mutex> lock(mtx);
                if (instance_ == NULL) {
                    Singleton* temp = operator new(sizeof(Singleton));  // memory allocate
										temp->member_ = x;  // construct
                    instance_ = temp;
                }
            }
            return instance_;
        }
        void Print() { 
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static Singleton* instance_;  //declare a static member variable    
};

Singleton* Singleton::instance_ = NULL;  //define a static member variable

temp的构造函数在对temp 的成员变量进行赋值时 temp->member_ = x,该操作和 instance_ = temp 可能会因为编译器的优化而重排顺序,如果 instance_ = temp 先于temp->member_ = x 执行

instance_ = temp;
temp->member_ = x

当线程A 在执行完 instance_ = temp; 后暂时挂起,线程B获取到 instance_ 后访问其成员变量 member_时,将会引发未定义的行为(如果是指针,将会发生core),因此我们需要保证 类Singleton 的所有成员变量的相关操作应该也是 volatile 的,于是代码如下

class Singleton {
    public :
        static volatile Singleton* GetInstance(int x = 0) {
            if (instance_ == NULL) {
                std::lock_guard<std::mutex> lock(mtx);
                if (instance_ == NULL) {
                    volatile Singleton* temp = new volatile Singleton(x);
                    instance_ = temp;
                }
            }
            return instance_;
        }
        void Print() { 
            std::cout << this->member_ << std::endl;
        }

    private:
        Singleton(int x = 3) : member_(x) {}
        int member_;
        static volatile Singleton* instance_;  //declare a static member variable    
};

volatile Singleton* Singleton::instance_ = NULL;  //define a static member variable

参考链接:https://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值