C++ 单例模式

黎 浩然/ 21 5 月, 2022/ C/C++, 计算机/COMPUTER/ 0 comments

众所周知的单例

class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

singleton *singleton::p = nullptr;

singleton* singleton::instance() {
    if (p == nullptr)
        p = new singleton();
    return p;
}

这是一个非常简单的实现,将构造函数声明为private或protect防止被外部函数实例化,内部有一个静态的类指针保存唯一的实例,实例的实现由一个public方法来实现,该方法返回该类的唯一实例。

当然这个代码只适合在单线程下,当多线程时,是不安全的。考虑两个线程同时首次调用instance方法且同时检测到p是nullptr,则两个线程会同时构造一个实例给p,这将违反了单例的准则。

懒汉和饿汉

单例分为两种实现方法:

  • 懒汉
    • 第一次用到类实例的时候才会去实例化,上述就是懒汉实现。
  • 饿汉
    • 单例类定义的时候就进行了实例化。

这里也给出饿汉的实现:

class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

singleton *singleton::p = new singleton();
singleton* singleton::instance() {
    return p;
}

当然这个是线程安全的,对于我们通常阐述的线程不安全,为懒汉模式,下面会阐述懒汉模式的线程安全代码优化。

加锁确保线程安全

#include <mutex>
using namespace std;

class singleton {
private:
    singleton() {}
    static singleton *p;
    static mutex *lock_;
public:
    static singleton *instance();
};

mutex *singleton::lock_ = new mutex();
singleton *singleton::p = nullptr;

singleton* singleton::instance() {
    lock_guard<mutex> guard(*lock_);
    if (p == nullptr)
        p = new singleton();
    return p;
}

在C++中加锁有个类不用手动管理unlock,那就是lock_guard,这里采用其进行加锁。

这种写法不会出现上面两个线程都执行到p==nullptr里面的情况,当线程A在执行new singleton()的时候,线程B如果调用了instance(),一定会被阻塞在加锁处,等待线程A执行结束后释放这个锁。从而是线程安全的。

但是这种写法性能非常低下,因为每次调用instance()都会加锁释放锁,而这个步骤只有当在第一次new singleton()才是有必要的,只要p被创建出来了,不管多少线程同时访问,if (p == nullptr)进行判断都是足够的(只是读操作,不需要加锁),没有线程安全问题,加了锁之后反而存在性能问题。

双重检查锁模式

上面写法是不管任何情况都会去加锁,然后释放锁,而对于读操作是不存在线程安全问题的,故只需要在第一次实例创建的时候加锁,以后不需要。

#include <mutex>
using namespace std;

class singleton {
private:
    singleton() {}
    static singleton *p;
    static mutex *lock_;
public:
    static singleton *instance();
};

mutex *singleton::lock_ = new mutex();
singleton *singleton::p = nullptr;

singleton* singleton::instance() {
    if(p == nullptr) {
        lock_guard<mutex> guard(*lock_);
        if(p == nullptr){
            p = new singleton;
        }
    }
    return p;
}

看起来上述代码非常美好,可是过了相当一段时间后,才发现这个漏洞。考虑以下语句

p = new singleton;

这条语句会导致三个事情的发生:

  1. 分配能够存储singleton对象的内存;
  2. 在被分配的内存中构造一个singleton对象;
  3. p指向这块被分配的内存。

你可能会认为这三个步骤是按顺序执行的,但实际上只能确定步骤1是最先执行的,步骤23却不一定。问题就出现在这。在汇编级别层面分析以下过程:

  • 线程A调用instance,执行第一次p的测试,获得锁,按照1,3,执行,然后被挂起。此时p是非空的,但是p指向的内存中还没有singleton对象被构造。
  • 线程B调用instance,判定p非空, 将其返回给instance的调用者。调用者对指针解引用以获得singleton,噢,一个还没有被构造出的对象。bug就出现了。

Share this Post

Leave a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注

*
*