设计模式之单例模式(C++)

设计模式之单例模式(C++实现)

1. 什么是Singleton?

设计模式中的Singleton,中文翻译是单例模式(也有翻译单件模式),指的是限制一个类只能实例化出一个对象。这种看是奇特使用类的方式在某些情况下是有用的,比如整个系统中只有一个状态管理的类,又比如在Windows中的任务管理器。这些应用场景下只需要一个类的实例,多个实例反而不方便管理,也可能存在某些潜在的冲突,在这些应用中Singleton就可以很好地得到应用。

2. 替代方案

Singleton很明显可以使用一个全局的变量来替换(C++),但是相比于全局变量它有以下的优势:

可以避免引入与全局变量产生冲突
使用单例模式可以使用到延迟加载(lazy allocation and initialization),在没有使用的时候可以节约资源

3. C++中实现

单例模式的一种常用实现如下:

1)考虑到需要只有一个实例,因此常常使用一个静态的变量来表示;
2)为了避免类的使用者new出多个实例,需要将构造函数声明为private
3)但是需要有一种可以得到这个对象的方式,一般使用getInstatnce()这个public的方法来获取这个实例

综合上述介绍,一个简单的实现如下 1.

//定义
class Singleton {
public:
    static Singleton* getInstance();
    void doSomething();
protected:
    Singleton();
private:
    static Singleton* _instance;
};

//实现
Singleton* Singleton::_instance = nullptr;

Singleton* Singleton::getInstance() {
    if (_instance == nullptr) {
        _instance = new Singleton;
    }
    return _instance;
}

void Singleton::doSomething() {
    std::cout << "Doing" << std::endl;
}

这个实现在单线程的环境下工作的很好,但是在多线程环境中可能存在着潜在的危险,考虑到两个线程同时运行到 if (_instance == nullptr),其中一个线程在这个时候被挂起,当另一个线程初始化_instance之后,唤醒挂起的线程,这时候该线程会继续创建一个实例,这与Singleton中单个实例的设计背道而驰了。

4. 解决方案

4.1 简单实现

既然上述的设计并非是线程安全的,那么我们在构造_instance的时候给它加锁不就好了吗?实现如下:

//定义
class Singleton {
public:
    static Singleton* getInstance();
    void doSomething();
protected:
    Singleton();
private:
    static Singleton* _instance;
};

//实现
Singleton* Singleton::_instance = nullptr;
std::mutex mutex;

Singleton* Singleton::getInstance() {
    // 加上mutex使得每次只有一个线程进入
    std::lock_guard<std::mutex> locker(mutex);
    if (_instance == nullptr) {
        _instance = new Singleton;
    }
    return _instance;
}

void Singleton::doSomething() {
    std::cout << "Doing" << std::endl;
}

这样又会引入一个新的令人不爽的地方,线程在getInstance调用时都需要等待其他线程完成访问,这样不利于多线程发挥出它应有的优势。仔细看一下实现,发现实际上真正需要加锁的只有new这一个步骤,这样在初始化完成之后所有的线程就不会在进入到if这个分支中了,于是我们修改为:

Singleton* Singleton::getInstance() {

    if (_instance == nullptr) {
        // 加上mutex使得每次只有一个线程进入
    std::lock_guard<std::mutex> locker(mutex);
        _instance = new Singleton;
    }
    return _instance;
}

但是这样做有一个潜在的危险,当线程A和B都运行到if(_instance == nullptr)之后A线程挂起,B线程运行到加锁,new出对象,之后A线程被唤醒,它也会加锁并再次创建一个对象,于是又会出现两个实例,这与单例模式相悖。

4.2 DCLP方式

既然还是会创建新的对象,那么我们在创建之前再次判断一下不就好了吗,于是引出新的一个概念称为 The Double-Checked Locking Pattern(DCL
P),具体实现如下:

Singleton* Singleton::getInstance() {

    if (_instance == nullptr) {
        // 加上mutex使得每次只有一个线程进入
    std::lock_guard<std::mutex> locker(mutex);
       if (_instance == nullptr)
         _instance = new Singleton;
    }
    return _instance;
}

接着上文的叙述,在A线程加锁之后发现B线程已经将_instance实例化了,于是_instance == nullptr为false,这样A就不会再次new出实例了,完美的解决了问题。

4.3 新的麻烦

使用DCLP就可以解决这个问题吗?事实上在某种程度上可以,但是不同的编译器在执行getInstance这个函数的时候有不同的处理方式,使得使用DCLP并不是一个适用于所有平台和编译器的完美解决方案,具体的细节十分复杂,读者可以参考下面这篇论文
C++ and the Perils of Double-Checked Locking2
文中给出了一个实现真正线程安全的一个实现(需要针对不同的操作系统实现),实现的模式是:

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance;
    ...                     // insert memory barrier
    if (tmp == NULL) {
        Lock lock;
        tmp = m_instance;
        if (tmp == NULL) {
            tmp = new Singleton;
            ...             // insert memory barrier
            m_instance = tmp;
        }
    }
    return tmp;
}

4.4 C++11的实现

C++11的一大亮点是引入了跨平台的线程库,通过线程库可以很好的完成上文中提到的问题。

4.4.1 实现方式一

这种实现方式实际上就是对上文中的DCLP的描述,实现代码如下:

std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release);
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}

另外还有其他的实现方式,更多的内容可以参考:Double-Checked Locking Is Fixed In C++11 3

4.4.2 推荐的实现方式

对于singleton的实现方式,在Effective C++中作者实现了

class S
{
    public:
        static S& getInstance()
        {
            static S    instance; 
            return instance;
        }
    private:
        S() {};             

        S(S const&)               = delete;
        void operator=(S const&)  = delete;

};

这种实现方式是线程安全的(C++11),在C++11的规范中有一下描述:

The C++11 standard §6.7.4:

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

也就是说C++11保证了静态变量在被初始化的时候强制线程保持同步,这种方式就实现了无锁完美的方案,具体内部的实现是编译器的事情,它可能使用DCLP的方式或者其他的方式完成。但是C++11提到的这个语义保证了单例模式的实现。
更多的内容可以参考4

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值