二、day2 从今天起,我们将开始学习设计模式的相关知识。在此前的网络编程和并发编程中,我们已经详细讲解了什么是单例模式,并介绍了其在 C++ 中的三种实现方式。本节将对之前学习的单例模式知识进行总结。
总体来说设计模式可以分为三大类:
创建型模式 :工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式
结构型模式 :适配器模式、过滤器模式、装饰模式、享元模式、代理模式、外观模式、组合模式、桥接模式
行为型模式 :责任链模式、命令模式、中介者模式、观察者模式、状态模式、策略模式、模板模式、空对象模式、备忘录模式、迭代器模式、解释器模式、访问者模式
参考:
恋恋风辰官方博客
1. 什么是单例模式? 单例模式(Singleton ),保证一个类仅有一个实例,并提供一个访问它的全局访问点,单例模式是在内存中仅会创建一次对象的设计模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Single2 { private : Single2 () {} Single2 (const Single2 &) = delete ; Single2& operator =(const Single2&) = delete ; public : static Single2& GetInst () { static Single2 single; return single; } };
在上面的代码块中,定义了一个Single2
类,Single2
类的默认构造函数被声明为私有,且删除拷贝构造函数和赋值运算符,确保Single2
类不能通过拷贝创建或赋值创建新的实例。
Single2
类只有一个公共静态方法 GetInst()
,用于获取Single2
类的唯一实例。
局部静态成员single
用于存储Single2
类的唯一实例,通过返回single
即可返回该实例。
单例模式的简单实现可总结为:
构造方法是私有的
对外暴露的获取访问是公有的静态的
唯一实例的存储方式是静态的
风险 :上述代码块(懒汉式 )生成了唯一实例,但在多线程方式下生成的实例可能会存在多个(如果多个线程同时调用GetInst()
时都会去实例化一个simgle
对象,使得Single2
类被重复实例化)
1.1 单例模式的分类
饿汉式 :类加载就会导致该单实例对象被创建
懒汉式 :类加载不会导致该单实例对象被创建,而是首次使用该对象时被创建
懒汉式 创建对象的方法是函数中创建静态局部变量,这样只有在对象第一次被使用时才会创建实例;而饿汉式 一般已经在类中提前声明了静态变量single
,这样在类加载时便已经提前创建好实例。
上述代码块的单例模式就是通过懒汉式实现的,静态变量single
在第一次使用Single2
类的GetInst()
时被创建,其声明周期随着进程结束而结束。
饿汉式单例模式实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Single2Hungry { private : Single2Hungry () { } Single2Hungry (const Single2Hungry&) = delete ; Single2Hungry& operator =(const Single2Hungry&) = delete ; public : static Single2Hungry* GetInst () { if (single == nullptr ) single = new Single2Hungry (); return single; } private : static Single2Hungry* single; };
饿汉模式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,可以避免线程安全问题。
多线程和单线程下进行测试 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Single2Hungry* Single2Hungry::single = Single2Hungry::GetInst (); void thread_func_s2 (int i) { cout << "this is thread " << i << endl; cout << "inst is " << Single2Hungry::GetInst () << endl; } void test_single2hungry () { cout << "s1 addr is " << Single2Hungry::GetInst () << endl; cout << "s2 addr is " << Single2Hungry::GetInst () << endl; for (int i = 0 ; i < 3 ; i++) { thread tid (thread_func_s2, i) ; tid.join (); } } int main () { test_single2hungry () }
输出为
1 2 3 4 5 6 7 8 s1 addr is 0x1e4b00 s2 addr is 0x1e4b00 this is thread 0 inst is 0x1e4b00 this is thread 1 inst is 0x1e4b00 this is thread 2 inst is 0x1e4b00
可见无论单线程还是多线程模式下,通过静态成员变量的指针实现的单例类 都是唯一的。饿汉式是在程序启动时就进行单例的初始化,这种方式也可以通过懒汉式调用,无论饿汉式还是懒汉式都存在一个问题,就是什么时候释放内存? 多线程情况下,释放内存就很难了,还有二次释放内存的风险。
1.2 懒汉式的改进 上面提到了懒汉式有一定的风险 :在多线程下可能会创建多个Single2
的实例,如果多个线程同时调用GetInst()
时都会去实例化一个simgle
对象,使得Single2
类被重复实例化。
通过对GetInst()
方法枷锁或者对Single2
类进行加锁 ,可以解决该风险,每个线程在进入方法前,都要等到别的线程都离开此方法,不会有两个线程同时进入此方法。
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 class SinglePointer { private : SinglePointer () { } SinglePointer (const SinglePointer&) = delete ; SinglePointer& operator =(const SinglePointer&) = delete ; public : static SinglePointer *GetInst () { if (single != nullptr ) { return single; } s_mutex.lock (); if (single != nullptr ) { s_mutex.unlock (); return single; } single = new SinglePointer (); s_mutex.unlock (); return single; } private : static SinglePointer *single; static mutex s_mutex; };
该段代码块通过双重检验枷锁进行加锁,避免了直接加锁造成的问题:每次去获取对象都需要先获取锁,并发性能非常地差。
双重检验枷锁:
如果已经实例化了,则不需要加锁,直接返回实例化对象
如果没有实例化对象则加锁,然后再判断一次有没有实例化
如果实例化了就解锁并返回实例化对象
如果没有实例化就初始化实例化对象,并解锁返回实例化对象
进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 SinglePointer *SinglePointer::single = nullptr ; std::mutex SinglePointer::s_mutex; void thread_func_lazy (int i) { cout << "this is lazy thread " << i << endl; cout << "inst is " << SinglePointer::GetInst () << endl; } void test_singlelazy () { for (int i = 0 ; i < 3 ; i++) { thread tid (thread_func_lazy, i) ; tid.join (); } } int main () { test_singlelazy (); }
输出为
1 2 3 4 5 6 this is lazy thread 0 inst is 0xbc1700 this is lazy thread 1 inst is 0xbc1700 this is lazy thread 2 inst is 0xbc1700
尽管多线程下懒汉式可能会创建多个Single2类实例的问题被解决,但无论懒汉式还是饿汉式,都有一个共同的问题 需要解决:什么时候释放内存?多线程下多次delete也会造成崩溃。
1.3 智能指针方法 使用智能指针方法自动回收内存的机制设计单例类
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 class SingleAuto { private : SingleAuto () { } SingleAuto (const SingleAuto&) = delete ; SingleAuto& operator =(const SingleAuto&) = delete ; public : ~SingleAuto () { cout << "single auto delete success " << endl; } static std::shared_ptr<SingleAuto> GetInst () { if (single != nullptr ) { return single; } s_mutex.lock (); if (single != nullptr ) { s_mutex.unlock (); return single; } single = std::make_shared <SingleAuto>(); s_mutex.unlock (); return single; } private : static std::shared_ptr<SingleAuto> single; static mutex s_mutex; };
SingleAuto
类的GetInst()
返回std::shared_ptr<SingleAuto>
类型的变量single
。因为single
是静态成员变量,所以会在进程结束时被回收。智能指针被回收时会调用内置指针类型的析构函数,从而完成内存的回收。
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 std::shared_ptr<SingleAuto> SingleAuto::single = nullptr ; mutex SingleAuto::s_mutex; void test_singleauto () { auto sp1 = SingleAuto::GetInst (); auto sp2 = SingleAuto::GetInst (); cout << "sp1 is " << sp1 << endl; cout << "sp2 is " << sp2 << endl; } int main () { test_singleauto (); }
输出:
1 2 sp1 is 0x1174f30 sp2 is 0x1174f30
智能指针方式不存在内存泄漏,但是有一个隐患 :单例类的析构函数是公有成员,如果被人手动调用会存在崩溃问题,比如将上边测试中的注释打开,程序会崩溃
1.4 辅助类智能指针单例模式 将析构函数私有化,在构造智能指针时指定删除器 ,通过传递一个辅助类或者辅助函数帮助智能指针回收内存时调用指定的析构函数。因为析构函数私有化以后,智能指针在引用计数归零后无法调用对象的析构函数进行销毁。所以必须指定一个删除器,该删除器是单例类的友元类,可以访问单例类的私有或公有成员,可以通过删除器间接调用单例类的析构函数。
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class SingleAutoSafe ;class SafeDeletor { public : void operator () (SingleAutoSafe *sf) { cout << "this is safe deleter operator()" << endl; delete sf; } }; class SingleAutoSafe { private : SingleAutoSafe () {} ~SingleAutoSafe () { cout << "this is single auto safe deletor" << endl; } SingleAutoSafe (const SingleAutoSafe &) = delete ; SingleAutoSafe &operator =(const SingleAutoSafe &) = delete ; friend class SafeDeletor ; public : static std::shared_ptr<SingleAutoSafe> GetInst () { if (single != nullptr ) { return single; } s_mutex.lock (); if (single != nullptr ) { s_mutex.unlock (); return single; } single = std::shared_ptr <SingleAutoSafe>(new SingleAutoSafe, SafeDeletor ()); s_mutex.unlock (); return single; } private : static std::shared_ptr<SingleAutoSafe> single; static mutex s_mutex; };
如果是提前声明SafeDeletor
,而先定义SingleAutoSafe
,会造成 incomplete type(类型不完整 )的错误,因为被提前声明的类,可以在后面定义,但在声明类前面定义的其他类中使用声明类时,只能使用声明类的指针,而不能创建声明类的实例。
SafeDeletor
类中**重载了()**,实现类模拟函数的作用。
SafeDeletor
要写在SingleAutoSafe
上边,并且SafeDeletor
要声明为SingleAutoSafe
类的友元类,这样就可以访问SingleAutoSafe
的析构函数了。在构造single
时制定了SafeDeletor()
,single
在回收时,会调用仿函数SafeDeletor()
,从而完成内存的销毁。同时,SingleAutoSafe
的析构函数为私有,无法被外界显式调用。
1.5 通用的单例模板类 通过声明单例的模板类,然后继承这个单例模板类的所有类就是单例类了,可以达到泛型编程提高效率的目的
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 36 37 38 39 40 41 template <typename T>class Single_T { protected : Single_T () = default ; Single_T (const Single_T<T> &st) = delete ; Single_T &operator =(const Single_T<T> &st) = delete ; ~Single_T () { cout << "this is auto safe template destruct" << endl; } public : static std::shared_ptr<T> GetInst () { if (single != nullptr ) { return single; } s_mutex.lock (); if (single != nullptr ) { s_mutex.unlock (); return single; } single = std::shared_ptr <T>(new T, SafeDeletor_T <T>()); s_mutex.unlock (); return single; } private : static std::shared_ptr<T> single; static mutex s_mutex; }; template <typename T>std::shared_ptr<T> Single_T<T>::single = nullptr ; template <typename T>mutex Single_T<T>::s_mutex;
模板类的静态成员变量要在头文件中初始化,而非模板类的静态成员变量一般在cpp文件中初始化。
应用:定义一个网络的单例类,继承上述模板类,并将构造和析构设置为私有,同时设置友元保证自己的析构和构造可以被友元类调用.
1 2 3 4 5 6 7 8 9 10 class SingleNet : public Single_T<SingleNet>{ private : SingleNet () = default ; SingleNet (const SingleNet &) = delete ; SingleNet &operator =(const SingleNet &) = delete ; ~SingleNet () = default ; friend class SafeDeletor_T <SingleNet>; friend class Single_T <SingleNet>; };
删除器SafeDeletor_T 和单例模板类Single_T 都是模板,需要提前定义。
测试:
1 2 3 4 5 6 7 void test_singlenet () { auto sp1 = SingleNet::GetInst (); auto sp2 = SingleNet::GetInst (); cout << "sp1 is " << sp1 << endl; cout << "sp2 is " << sp2 << endl; }
1.6 总结
上面实现的单例模板其实可以通过C++11新特性更加简化,有两种新的实现方式,可以参考网络编程后面的文章。而且上面通过智能指针双重检测具有一定的风险,我们可以通过原子类型并添加内存序进行修改,这部分可以参考并发编程的内容。
1. 为什么要有单例模式?
使用单例模式的原因
资源控制:单例模式可以用来控制系统中的资源,例如数据库连接池或线程池,确保这些关键资源不会被过度使用。
内存节省:当需要一个对象进行全局访问,但创建多个实例会造成资源浪费时,单例模式可以确保只创建一个实例,节省内存。
共享:单例模式允许状态或配置信息在系统的不同部分之间共享,而不需要传递实例。
延迟初始化:单例模式支持延迟初始化,即实例在首次使用时才创建,而不是在类加载时。
一致的接口:单例模式为客户端提供了一个统一的接口来获取类的实例,使得客户端代码更简洁。
易于维护:单例模式使得代码更易于维护,因为所有的实例都使用相同的实例,便于跟踪和修改变更。
单例模式的应用场景
配置管理器:在应用程序中,配置信息通常只需要读取一次,并全局使用。单例模式用于确保配置管理器只被实例化一次。
日志记录器:一个系统中通常只需要一个日志记录器来记录所有的日志信息,使用单例模式可以避免日志文件的重复写入。
数据库连接池:数据库连接是一种有限的资源,使用单例模式可以确保数据库连接池的唯一性,并且能够重用连接,减少连接创建和销毁的开销。
线程池:类似于数据库连接池,线程池也是有限的资源,使用单例模式可以避免创建过多的线程,提高应用程序的并发性能。
任务调度器:在需要全局调度和管理的场景下,如定时任务调度器,单例模式提供了一个集中的管理方式。
网站的计数器:一般也是采用单例模式实现,否则难以同步。
2. 单例模式中GetInst()为什么是静态的?
1)静态方法可以通过类名直接访问,无需创建类的实例。这样可以方便地获取唯一实例,而不需要先实例化类。
2)类的构造函数已经被私有化,无法直接实例化对象,智能通过定义静态成员函数的方式通过类名::方法名的方式进行构造访问
3)静态局部变量的方式是线程安全的,能确保在多线程环境下也只会创建一个实例
2. 单例模式的三种实现方式 2.1 通过std::call_once和std::once_flag实现 该方式实现的单例模式可以添加自定义的删除器。
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 #pragma once #include <memory> #include <mutex> #include <iostream> template <typename T>class Singleton {protected : Singleton () = default ; Singleton (const Singleton<T>&) = delete ; Singleton& operator =(const Singleton<T>& st) = delete ; static std::shared_ptr<T> _instance; public : static std::shared_ptr<T> GetInstance () { static std::once_flag s_flag; std::call_once (s_flag, [&]() { _instance = std::shared_ptr <T>(new T); }); return _instance; } void PrintAddress () { std::cout << _instance.get () << std::endl; } ~Singleton () { std::cout << "this is singleton destruct" << std::endl; } }; template <typename T>std::shared_ptr<T> Singleton<T>::_instance = nullptr ;
这是我通过C++11新特性实现的AsioIOServicePool
线程池单例模式,主要通过构造一个静态成员函数和一个静态成员变量实现:
1 2 3 4 5 6 7 8 9 static std::shared_ptr<AsioIOServicePool> _instance;static std::shared_ptr<AsioIOServicePool> GetInstance () { static std::once_flag s_flag; std::call_once (s_flag, [&]() { _instance = std::shared_ptr <AsioIOServicePool>(new AsioIOServicePool, SafeDeletor ()); }); return _instance; }
2.2 静态成员函数实现 第二种方法通过定义一个静态成员函数构造一个逻辑层的单例模式。
相比于5.1实现的单例模式,该方法实现的单例模式无法添加删除器。
我们只需要定义一个静态成员函数GetInstance()
,在该函数中返回局部静态变量instance
,instance
是LogicSystem
的实例。该方法只能在C++11及以后的平台才可以使用,因为返回局部静态变量只有在C++11及以上是线程安全的。
在C++11之前,多线程同时首次 调用GetInstance()
时,可能触发竞态初始化 ,导致未定义行为(如重复初始化、值覆盖)。
在C++11及之后,多个线程首次初始化时,仅有一个线程执行初始化 ,其他线程等待直至完成。
通过原子标志 + 互斥锁 实现,仅首个线程执行初始化,后续线程通过原子操作感知已初始化,避免加锁,确保 “初始化完成” 的状态全局可见
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 36 37 38 #pragma once #include <queue> #include <thread> #include "CSession.h" #include <map> #include <functional> #include "Const.h" #include <json/json.h> #include <json/value.h> #include <json/reader.h> class LogicNode ;class CSession ;typedef std::function<void (std::shared_ptr<CSession>, const short & msg_id, const std::string& msg_data)> FunCallBack;class LogicSystem { private : LogicSystem (); ~LogicSystem (); LogicSystem (const LogicSystem&) = delete ; LogicSystem& operator =(const LogicSystem&) = delete ; void RegisterCallBacks () ; void HelloWordCallBack (std::shared_ptr<CSession>, const short & msg_id, const std::string& msg_data) ; void DealMsg () ; std::queue<std::shared_ptr<LogicNode>> _msg_que; std::mutex _mutex; std::condition_variable _consume; std::thread _worker_thread; bool _b_stop; std::map<short , FunCallBack> _fun_callback; public : void PostMsgToQue (std::shared_ptr<LogicNode> msg) ; static LogicSystem& GetInstance () ; };
函数实现:
1 2 3 4 5 LogicSystem& LogicSystem::GetInstance () { static LogicSystem instance; return instance; }
2.3 智能指针双重检测实现 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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 class ingle_T ;template <typename T>class SafeDeletor {public : void operator () (Single_T<T>* st) { delete st; } }; template <typename T>class Single_T { protected : Single_T () = default ; Single_T (const Single_T<T> &st) = delete ; Single_T &operator =(const Single_T<T> &st) = delete ; ~Single_T () { cout << "this is auto safe template destruct" << endl; } public : static std::shared_ptr<T> GetInst () { if (single != nullptr ) { return single; } s_mutex.lock (); if (single != nullptr ) { s_mutex.unlock (); return single; } single = std::shared_ptr <T>(new T, SafeDeletor <T>()); s_mutex.unlock (); return single; } private : static std::shared_ptr<T> single; static mutex s_mutex; }; template <typename T>std::shared_ptr<T> Single_T<T>::single = nullptr ; template <typename T>mutex Single_T<T>::s_mutex;
该方式是通过智能指针对懒汉式单例模式 的优化修改,但是该方法存在一定的风险:
1 2 3 4 5 6 7 8 9 10 11 12 std::shared_ptr<SingleAuto> SingleAuto::single = nullptr ; std::mutex SingleAuto::s_mutex; void TestSingle () { std::thread t1 ([]() { std::cout << "thread t1 singleton address is 0X: " << SingleAuto::GetInst() << std::endl; }) ; std::thread t2 ([]() { std::cout << "thread t2 singleton address is 0X: " << SingleAuto::GetInst() << std::endl; }) ; t2. join (); t1. join (); }
在该段代码下,虽然两次输出的地址都是同一个,但是我们的单例会存在安全隐患 。1处和4处代码存在线程安全问题,因为在4处通过智能指针new一个对象再赋值给变量时会存在多个指令顺序:
第一种情况
1 2 3 1 为对象allocate一块内存空间 2 调用construct构造对象 3 将构造到的对象地址返回
第二种情况
1 2 3 1 为对象allocate一块内存空间 2 先将开辟的空间地址返回 3 调用construct构造对象
如果是第二种情况,在4处还未构造对象就将地址返回赋值给single,而此时有线程运行至1处判断single不为空直接返回单例实例,如果该线程调用这个单例的成员函数就会崩溃(因为对象还未构造,只返回了为该对象开辟的地址)。
我们通过我们这节的知识修改该单例模板:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <iostream> #include <memory> #include <mutex> #include <atomic> template <typename T>class Singleton { protected : Singleton () = default ; Singleton (const Singleton<T>&) = delete ; Singleton& operator =(const Singleton<T>&) = delete ; public : ~Singleton () { std::cout << "this is auto safe template destruct" << std::endl; } static std::shared_ptr<T> GetInstance () { if (_b_init.load (std::memory_order_acquire)) { return instance; } s_mutex.lock (); if (_b_init.load (std::memory_order_relaxed)) { s_mutex.unlock (); return instance; } instance = std::shared_ptr <T>(new T ()); _b_init.store (true , std::memory_order_release); s_mutex.unlock (); return instance; } void PrintAddress () { std::cout << instance.get () << std::endl; } private : static std::shared_ptr<T> instance; static std::mutex s_mutex; static std::atomic<bool > _b_init; }; template <typename T>std::shared_ptr<T> Singleton<T>::instance = nullptr ; template <typename T>std::mutex Singleton<T>::s_mutex; template <typename T>std::atomic<bool > Singleton<T>::_b_init = false ;
这种方法实现的单例模板解决了线程安全的问题。
这三种方式均可用于实现单例模式,但如果不需要额外添加删除器之类的操作,我们用第二种方法即可,第一种和第三种均可以通过 std::shared_ptr
的构造函数手动添加删除器。