C++中的单例模式

本文详细探讨了C++中单例模式在单线程和多线程环境下的实现,从基础版本到加锁版本,再到通过内存屏障优化的版本。还介绍了Meyers Singleton在C++11后的正确性和线程安全性,以及在不确定编译器行为时,如何利用pthread_once确保初始化的唯一性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单线程版本

template<typename T>
class Singleton {
public:
    T &getInstance() {
        if (value_ == nullptr) {//判断语句
            vaule_ = new T();
        }
        return *value_;
    }
private:
    Singleton();
    ~Singleton();
    static T *value_;
};

这个版本只适合单线程使用, 因为如果在多线程环境下可能两个线程同时执行到上面的判断语句, 进而同时进入if为真的分支,最后两个线程产生了各自的实例, 这就破坏了单例的定义。

改进:多线程版本

template<typename T>
class Singleton {
public:
    T &getInstance() {
        MutexLock lock(mutex_); //RAII
        if (value_ == nullptr) {
            value_ = new T();
        }
        return *value_;
    }
private:
    Singleton();
    ~Singleton();
    Mutex mutex_;
    static T *value_;
}

既然在判断语句这里存在竞争, 那就直接加锁好了, 这样的处理方法总是正确的, 但是效率确实堪忧的, 因为所有的线程都要争这一个锁, 即使是单例实例已经构造出来了。注意在上面的写法中为了简化代码, 我用了RAII的写法, Mutex以及MutexLock的定义没有给出, 但是应该不难得到他们简单的定义。

改进:更好的效率

上个一个小节中写的单例模式之所以低效, 是因为即使单例已经被构造出来, 每个线程还是要争用锁, 根据这样的观察, 我们有以下改进:

template<typename T>
class Singleton {
public:
    T &getInstance() {
        if (value_ == nullptr) {
            MutexLock lock(mutex_);
            if (value_ == nullptr) {
                value_ = new T();//#1
            }
        }
        return *value_;
    }
private:
    Singleton();
    ~Singleton();
    static T *value_;
    Mutex mutex_;
}

上面的这种写法在C++11之前是不能保证正确的, 原因是这样的:上面标号为1的代码在底层执行的时候相当于下面的代码:

T* p = static_cast<T*>(operator new(sizeof(T)));
new (p) T();//#2
value_ = p; //#3

先为指针申请内存是确定的, 但是标准却没有指定2和3 的执行顺序, 那如果先执行2,再执行3是没有问题的, 但是如果反过来, 先执行3, 那其他线程判断value_为非空, 但其实现在的value_ 并没有赋有效值,从而产生错误。要想改正这个错误, 就需要把value_ = new T()手动拆分成上面的三段式写法, 并且在2和3之间加入内存屏障, 强制保证2在3之前执行。
至此, 我们就得到了一个适合多线程版本的, 而且效率也不错的单例模式,锁在牺牲一定效率的前提先保证了多线程单例模式的正确性, 但是人们总是在探索正确而更加高效的写法, 比如, 如何在不用锁的情况下写出一个单例模式:

Meyers Singleton

template<typename T>
class Singleton {
public:
    T &getInstance() {
        static T value;
        return value;
    }
private:
    Singleton();
    ~Singleton();
}

在C++11之后这种写法是正确的, 而且适用于多线程版本。但是在C++11之前, 这种写法不适用于多线程版本, 原因在于C++11之前并没有规定local static variable的内存模型, 好多编译器的实现是one check, 就拿上面的例子来说, 在旧版本的编译器上实现可能是这样的:

bool initialized = false;
char value[sizeof(T)];

T& getInstance()
{
    if (!initialized)
    {
       initialized = true;
       new (value) T();
    }
    return *(reinterpret_cast<T*>(value));
}

这种窘境我们在前面已经看到过了, 两个线程可能同时检查initialized的值, 并且同时进入if分支, 造成两个线程各自为政的局面, 所谓单例中的“单”也就不复存在了。但是在C++11之后, 标准规定:某个线程的局部静态变量必须被初始化之后才能被其他线程访问, 所以one check的实现方式就一去不复返了, 自然能保证单例的正确。

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

最安心的方法: pthread_once

template<typename T>
class Singleton : Nocopyable
{
public:
    static T& getInstance()
    {
        threads::pthread_once(&once_control_, init);
        return *value_;
    }

private:
    static void init()
    {
        value_ = new T();
    }

    Singleton();
    ~Singleton();

    static pthread_once_t  once_control_;
    static T*              value_;
};

template<typename T>
pthread_once_t Singleton<T>::once_control_ = PTHREAD_ONCE_INIT;

template<typename T>
T* Singleton<T>::value_ = NULL;

posix规范里面的pthread_once能够保证注册的函数只运行一次, 不管C++的版本如何, 只要系统支持pthread, 这种方法就是最直接的。

附录1:

Mutex以及MutexLock定义:

class Mutex {
public:
    Mutex() {
        int err = 0;
        err = pthread_mutex_init(&mutex_, nullptr);
        if (err < 0) {
            //error checking
        }
    }
    void lock() {
        pthread_mutex_lock(&mutex_);
    }
    void unlock() {
        pthread_mutex_unlock(&mutex_);
    }
    ~Mutex() {
        pthread_mutex_destroy(&mutex_);
    }
    pthread_mutex_t *getInternalMutex() {
        return &mutex_;
    }
private:
    pthread_mutex_t mutex_;
};

class MutexLock {
public:
    MutexLock(Mutex &mutex) :mutex_(mutex) {
        mutex_.lock();
    }
    ~MutexLock() {
        mutex_.unlock();
    }
private:
    Mutex &mutex_;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值