• C++设计模式之---单例模式


    本文章会阐述C++最常用的设计模式—单例模式,从分类、线程安全的角度,分析有哪些解决线程安全的单例模式方案。

    1、众所周知的单例
    大家比较熟知的单例模式如下所示:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这是一个非常简单的实现,将构造函数声明为private或protect防止被外部函数实例化,内部有一个静态的类指针保存唯一的实例,实例的实现由一个public方法来实现,该方法返回该类的唯一实例。
    当然这个代码只适合在单线程下,当多线程时,是不安全的。考虑两个线程同时首次调用instance方法且同时检测到p是nullptr,则两个线程会同时构造一个实例给p,这将违反了单例的准则。

    2、懒汉与恶汉
    单例分为两种实现方法:
    懒汉
    第一次用到类实例的时候才会去实例化,上述就是懒汉实现。
    饿汉
    单例类定义的时候就进行了实例化。
    这里给出饿汉的实现:

    class singleton {
    private:
        singleton() {}
        static singleton *p;
    public:
        static singleton *instance();
    };
    
    singleton *singleton::p = new singleton();
    singleton* singleton::instance() {
        return p;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    3、多线程加锁
    在C++中加锁有个类实现原理采用RAII,不用手动管理unlock,那就是lock_guard,这里采用其进行加锁。

    class singleton {
    private:
        singleton() {}
        static singleton *p;
        static mutex lock_;
    public:
        static singleton *instance();
    };
    
    singleton *singleton::p = nullptr;
    
    singleton* singleton::instance() {
        lock_guard<mutex> guard(lock_);
        if (p == nullptr)
            p = new singleton();
        return p;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

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

    4、双重检查锁模式
    上面写法是不管任何情况都会去加锁,然后释放锁,而对于读操作是不存在线程安全的,故只需要在第一次实例创建的时候加锁,以后不需要。下面先看一下DCLP的实现:

    singleton* singleton::instance() {
    	if(p == nullptr) {  // 第一次检查
    		Lock lock;
    		if(p == nullptr){ // 第二次检查
    			p = new singleton;
    		}
    	}
    	return p;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    基于上述,我们可以写出双重检查锁+自动回收:

    class singleton {
    private:
        singleton() {}
    
        static singleton *p;
        static mutex lock_;
    public:
        static singleton *instance();
    
        // 实现一个内嵌垃圾回收类
        class CGarbo
        {
        public:
            ~CGarbo()
            {
                if(singleton::p)
                    delete singleton::p;
            }
        };
        static CGarbo Garbo; // 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
    };
    
    singleton *singleton::p = nullptr;
    singleton::CGarbo Garbo;
    std::mutex singleton::lock_;
    
    singleton* singleton::instance() {
        if (p == nullptr) {
            lock_guard<mutex> guard(lock_);
            if (p == nullptr)
                p = new singleton();
        }
        return p;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    DCLP的关键在于,大多数对instance的调用会看到p是非空的,因此甚至不用尝试去初始化它。因此,DCLP在尝试获取锁之前检查p是否为空。只有当检查成功(也就是p还没有被初始化)时才会去获得锁,然后再次检查p是否仍然为空(因此命名为双重检查锁)。第二次检查是必要,因为就像我们刚刚看到的,很有可能另一个线程偶然在第一次检查之后,获得锁成功之前初始化p。

    看起来上述代码非常美好,可是过了相当一段时间后,才发现这个漏洞,原因是:内存读写的乱序执行(编译器问题)。

    再次考虑初始化p的那一行:
    p = new singleton;

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

    分配能够存储singleton对象的内存;
    在被分配的内存中构造一个singleton对象;
    让p指向这块被分配的内存。
    可能会认为这三个步骤是按顺序执行的,但实际上只能确定步骤1是最先执行的,步骤2,3却不一定。问题就出现在这。

    线程A调用instance,执行第一次p的测试,获得锁,按照1,3,执行,然后被挂起。此时p是非空的,但是p指向的内存中还没有Singleton对象被构造。
    线程B调用instance,判定p非空, 将其返回给instance的调用者。调用者对指针解引用以获得singleton,噢,一个还没有被构造出的对象。bug就出现了。
    DCLP能够良好的工作仅当步骤一和二在步骤三之前被执行,但是并没有方法在C或C++中表达这种限制。这就像是插在DCLP心脏上的一把匕首:我们需要在相对指令顺序上定义限制,但是我们的语言没有给出表达这种限制的方法。

    5、memory barrier指令
    DCLP问题在C++11中,这个问题得到了解决。

    因为新的C++11规定了新的内存模型,保证了执行上述3个步骤的时候不会发生线程切换,相当这个初始化过程是“原子性”的的操作,DCL又可以正确使用了。

    C++11之前解决方法是barrier指令。要使其正确执行的话,就得在步骤2、3直接加上一道memory barrier。强迫CPU执行的时候按照1、2、3的步骤来运行。

    第一种实现:
    基于operator new+placement new,遵循1,2,3执行顺序依次编写代码。

    // method 1 operator new + placement new
    singleton *instance() {
        if (p == nullptr) {
            lock_guard<mutex> guard(lock_);
            if (p == nullptr) {
                singleton *tmp = static_cast<singleton *>(operator new(sizeof(singleton)));
                new(tmp)singleton();
                p = tmp;
            }
        }
        return p;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    第二种实现:
    基于直接嵌入ASM汇编指令mfence,uninx的barrier宏也是通过该指令实现的。

    #define barrier() __asm__ volatile ("lwsync")
    singleton *singleton::instance() {
        if (p == nullptr) {
            lock_guard<mutex> guard(lock_);
            barrier();
            if (p == nullptr) {
                p = new singleton();
            }
        }
        return p;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    通常情况下是调用cpu提供的一条指令,这条指令的作用是会阻止cpu将该指令之前的指令交换到该指令之后,这条指令也通常被叫做barrier。 上面代码中的asm表示这个是一条汇编指令,volatile是可选的,如果用了它,则表示向编译器声明不允许对该汇编指令进行优化。lwsync是POWERPC提供的barrier指令。

  • 相关阅读:
    HTML小游戏4 —— 简易版英雄联盟(附完整源码)
    a16z:推翻互联网的偶然君主制,如何设计Web3平台治理?
    深度学习网络模型——DenseNet模型详解与代码复现
    Axure药企内部管理平台+企业内部管理系统平台
    Java 多线程(四):锁(二)
    IPO解读丨转向国内帐篷市场,泰鹏智能能否抓住露营经济的红利?
    system与excel族函数区别
    【Java基础面试三十三】、接口和抽象类有什么区别?
    8、AI医生案例
    Spring框架系列(3) - 深入浅出Spring核心之控制反转(IOC)
  • 原文地址:https://blog.csdn.net/qq_41920323/article/details/133212879