游戏设计模式:单例模式(C++实现)
前言:
本文将探讨单例设计模式,单例类的懒汉模式/饿汉模式,单例类的多线程安全性,最后将利用C++模板减少单例类代码量。
本文假设有一个Manager管理类,并以此为探究单例类的设计模式。
懒汉模式
懒汉模式:顾名思义,是一种典型的拖延(lazy)策略。当第一次要用单例类的时候,再产生实例。
类声明:
class Manager{ public: ~Manager(); //提供单例对象访问 static Manager* getInstance(); //删除单例对象 static void deleteInstance(); void dosomething();
protected: //构造函数声明为 保护方法 Manager(); //单例对象指针 static Manager* s_Manager; };
//单例对象指针初始化为nullptr,防止指向了未定义的数据 Manager* Manager::s_Manager = nullptr; //提供单例类对象访问 Manager* Manager::getInstance(){ //当没有存在实例时(一般是指准备第一次用)时,才生成新实例 if(!s_Manager)s_Manager = new CacheManger(); return s_Manager; } //删除单例类 void Manager::deleteInstance(){ if(s_Manager){ deleted s_Manager; s_Manager = nullptr;//别忘了赋予空指针,否则指向未定义数据 } } void Manager::dosomething(){ //dosometing }
这样我们就能在平时的程序用
Manager::getInstance()->dosomething();
来运用单例类来做某些操作了。
懒汉模式with线程安全
但是上面的例子,并不能保证线程安全。
假如没有实例时,然后某两个线程都几乎同时使用getInstance(),那么很可能会产生2份实例,其中一份还会变成泄露的内存。
为了解决线程安全问题,自然想到用锁:(本文使用了C++11 <mutex>的std::mutex作为互斥锁,在类额外增加了一个静态变量std::mutext s_mtx;)
//提供单例类对象访问 Manager* Manager::getInstance() { if (!s_Manager) { //上锁 std::lock_guard<std::mutex> lock(s_mtx); //当没有存在实例时(一般是指准备第一次用)时,才生成新实例 if (!s_Manager) { s_Manager = new Manager(); } //解锁 } return s_Manager; } //删除单例类 void Manager::deleteInstance() { if (s_Manager) { //上锁 std::lock_guard<std::mutex> lock(s_mtx); if (s_Manager) { delete s_Manager; //别忘了赋予空指针,否则指向未定义的数据 s_Manager = nullptr; } //解锁 } }
为什么不是(上锁,检查,操作,解锁)或者(检查,上锁,操作,解锁),而是使用了双重检查(检查,上锁,检查,操作,解锁)?
- 上锁的成本远远比检查空指针要高,且当需要产生实例时才需要锁操作。而实际上大量多次使用getInstance时(因为已经产生了实例)并不需要上锁,若先上锁,则会严重造成性能阻塞。
- 仅仅是检查后再上锁,则根本没有做到任何线程安全。
饿汉模式
饿汉模式与懒汉模式相反,是程序一开始就生成唯一实例。这样就不用检查是否存在实例,而且也无需考虑产生实例时的线程安全。
class Manager { public: ~Manager(); //提供单例对象访问 static Manager& getInstance(); void dosomething(); protected: //构造函数声明为 保护方法 Manager(); //单例对象指针 static Manager s_Manager; }; //提供单例类对象访问 Manager& Manager::getInstance(){ return s_Manager; }
使用方法:
Manager::getInstance().dosomething();
可以看到代码比懒汉模式简单多了。
在大量使用检查空指针造成的性能瓶颈而内存始终充足时,可以考虑使用饿汉模式
Meyers Singleton(目前最推荐的C++单例写法)
class Singleton { public: static Singleton& Instance() { static Singleton theSingleton; return theSingleton; } private: Singleton(); Singleton(Singleton const&); Singleton& operator=(Singleton const&); ~Singleton(); };
这段代码很简单,虽然看上去和懒汉模式类似,只是static变量的位置从类移动到了实例获取函数内。
但是实际上由于C++的机制,当第一次调用该函数时,实例才会被构建出来;而且C++11规定,在一个线程开始 local static 对象的初始化后到完成初始化前,其他线程执行到这个 local static 对象的初始化语句就会等待,直到该 local static 对象初始化完成。
这样就可以得到饿汉模式的线程安全,又可以有懒汉模式的按需分配的功能。
应用场景注意
单例类设计模式算是比较经典的一个模式,但是需要注意,它并不是想象中那么美好。
- 它是一种换皮的全局变量。
- 它促进了耦合。
- 它可能对并发不友好(取决于你使用的单例写法)。
一些替代方案:
- 当你仅需要全局可见的方法时,应该用类静态方法而不是一个类实例。
- 尽可能为实例提供其它便捷的访问方式(传参/基类获取/服务定位器获取等),而不是通过提供全局可见的访问方式。
- 如果你只是需要类保证只有唯一对象而不需要全局性,那么应对外封闭获取实例接口(其实就是不可全局获取实例的)。
所以要注意单例类设计模式不应被泛用,通过上面的替代方案多多少少也就减少了很多不必要的单例设计。
游戏架构&游戏设计模式系列-其他文章:
作者:KillerAery
出处:http://www.cnblogs.com/KillerAery/
本文版权归作者和博客园共有,未经作者同意不可擅自转载,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了