在程序设计中,难免有一种类会反复被实例化调用。如果这种类被反复实例化,会浪费很多内存空间和实例化过程中的性能下降。然而,单例模式很好的解决了这个问题。

下面来看一下懒汉单例模式实现

单例模式(线程不安全)

class Singleton
{
public:
    static Singleton* get_instance()
    {
        if(!m_instance) m_instance = new Singleton();
        return m_instance;
    }
private:
    static Singleton* m_instance;
    Singleton(){}
    ~Singleton(){}
    Singleton(const Singleton& s) = delete;
    Singleton& operator=(const Singleton& s) = delete;
};

Singleton* Singleton::m_instance;

上面的写法在单线程下是没有问题的,但是多线程情况下,无法保证只生成一个实力,因为我们知道get_instance中line6处存在对临界资源的访问。有关线程安全的讨论在下面会讲到。

单例模式(线程安全)

1. 加锁

刚刚我们说道,get_instance中line6处存在对临界资源的访问。对于临界资源的反问,需要加锁进行处理

class Singleton
{
public:
    static Singleton* get_instance()
    {
        if(!m_instance)
        {
            lock_guard<mutex> lg(m_mutex);
            m_instance = new Singleton();
        }
        return m_instance;
    }
private:
    static Singleton* m_instance;
    static mutex m_mutex;
    Singleton(){}
    ~Singleton(){}
    Singleton(const Singleton& s) = delete;
    Singleton& operator=(const Singleton& s) = delete;
};

Singleton* Singleton::m_instance;
mutex Singleton::m_mutex;

2.双检测锁

同一时刻可能有多个线程尝试拿到锁,但只有第一个线程会拿到锁,后面的线程拿不到锁会阻塞,问题是,其他阻塞的线程总有拿到锁的一天,那么这些就都会进入if里面执行里面的语句。

解决方法很简单,只需要在if里面在判断一次指针是否已经分配内存即可。

class Singleton
{
public:
    static Singleton* get_instance()
    {
        if(!m_instance)
        {
            lock_guard<mutex> lg(m_mutex);
            if(m_instance) return m_instance;
            m_instance = new Singleton();
        }
        return m_instance;
    }
private:
    static Singleton* m_instance;
    static mutex m_mutex;
    Singleton(){}
    ~Singleton(){}
    Singleton(const Singleton& s) = delete;
    Singleton& operator=(const Singleton& s) = delete;
};

Singleton* Singleton::m_instance;
mutex Singleton::m_mutex;

3.避免指令重排

指令重排详见:指令重排内存顺序

简单来说,在if语句里,为了优化操作,编译器会将语句执行顺序进行重新排序,比如上面的代码,申请堆内存放在读取if中m_instance指针的前面。

这是我们不愿意看到的,为了解决这一问题,我们引入了atomic(C++11)解决。

在初始化时,构造函数参数为空,内存排序属性默认为memory_order_seq_cst。简单来说我初始化了static atomic<Singleton*> m_instance;,我先后执行了读取和赋值操作,这两个的操作的先后顺序是不会被指令重排优化的。

class Singleton
{
public:
    static Singleton* get_instance()
    {
        Singleton* temp = m_instance.load();
        if(!temp)
        {
            lock_guard<mutex> lg(m_mutex);
            temp = m_instance.load();
            if(temp) return temp;
            m_instance.store(temp = new Singleton());
        }
        return temp;
    }
private:
    static mutex m_mutex;
    static atomic<Singleton*> m_instance;
    Singleton(){}
    ~Singleton(){}
    Singleton(const Singleton& s) = delete;
    Singleton& operator=(const Singleton& s) = delete;
};

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

至此,我们就实现了一个线程安全的懒汉单例模式。

C++11新特性

C++11有一个新特性,那就是在函数中static变量的初始化是线程安全的

那么实现一个线程安全的懒汉单例模式就简单很多了。

class Singleton
{
public:
    static Singleton* get_instance()
    {
        static Singleton* m_instance = new Singleton();
        return m_instance;
    }
private:
    Singleton(){}
    ~Singleton(){}
    Singleton(const Singleton& s) = delete;
    Singleton& operator=(const Singleton& s) = delete;
};

参考资料

CSDN:指令重排
cppreference:内存顺序

Categories:

Tags:

还没发表评论,快来发表第一个评论吧~

发表回复