一般用 C++ and the Perils of Double-Checked Locking这篇文章,老外写的,加了注释如下:
C++ and the Perils of Double-Checked Locking.pdf
比较沙雕的翻译如下:
自从C++11出来后,一切都变了,以前叫人家小甜甜,现在叫牛夫人:
std::atomic<Singleton*> Singleton::m_instance; std::mutex Singleton::m_mutex; Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(); if (tmp == nullptr) { std::lock_guard<std::mutex> lock(m_mutex); tmp = m_instance.load(); if (tmp == nullptr) { tmp = new Singleton; m_instance.store(tmp); } } return tmp; }
实际上现在是2021年,9年前的东西你应该更加熟悉,那么最简洁的方法就是:
Singleton* Singleton::m_instance; Singleton* Singleton::getInstance() { // 用于call_once的局部静态变量 static std::once_flag oc; std::call_once(oc, [&] { m_instance = new Singleton();}); return m_instance; }
上面例子都是班门弄斧来自:https://blog.csdn.net/10km/article/details/49777749
有的沙雕说,我直接定义一个静态的返回不就行了嘛?我多聪明啊,而且还有StackOverflow加持:
https://stackoverflow.com/questions/2576022/efficient-thread-safe-singleton-in-c
static single& get_instance() { static single thissingle; return thissingle; }
这个沙雕的做法还是可以的。这里用了一个特性:
全局static是在程序启动的时候初始化,类内static和局部static变量是在第一次运行到的时候初始化。
但是,
如果类构建的时候要带参数,比如日志类,要带个目录,这个static就傻眼了!
并且,据说(大家睁大眼睛,自辩真伪),在局部作用域下的静态变量在编译时,编译器会创建一个附加变量标识静态变量是否被初始化,会被编译器变成像下面这样(伪代码):
static single& get_instance() { static bool is_inited = false; static uninitialized single thissingle; if(!is_inited){ is_inited = true; new (&s) single; } return thissingle; }
这里有竞争条件,两个线程同时调用 instance() 时,一个线程运行到 if 语句进入后还没设 is_inited 值,此时切换到另一线程,is_inited值还是 false,同样进入到 if 语句里初始化变量,两个线程都执行了这个单例类的初始化,就不再是单例了。
那还有个问题,这个new出来的东西怎么释放,这个也好办,内部新建一个私有类,然后创建一个静态成员,在析构的时候自动delete:
private: // 用来回收垃圾,释放内存 class Release { public: ~Release(){ if (KrShareMem::m_pInstance != nullptr){ delete KrShareMem::m_pInstance; } } }; static Release m_MemRelease;
注意在cpp当作一般静态成员初始化 m_MemRelease 即可。
实际上上面搞那么多,个人认为是有些湖里花哨的。其实只要最简单的 static myclass instance,或者*instance = new myclass;即可。
啥互斥也不搞,只要进入多线程之前调用一下这些getInstance()初始化一次就什么都搞定了。
因为程序流程是程序员控制的,只要能保证前期能创建,就不存在什么乱七八糟抢占多实例问题。
搞来搞去这么复杂,不是给自己找麻烦嘛?!
有好主意的欢迎留言交流
==================== 单例加入初始化参数 ====================
留言有个扛精,号称写了几十万行代码,说单例就要把初始化参数写死到类里面。那么我们来举一个例子。
比如我们有一个INI读写类,因为读写同一个文件,那做成单例肯定有好处,不会多个实例访问。代码如下:
class Config : public QObject{ public: // 单实例 static Config& i(); void save(const QString &key,const QVariant &value); };
基本没问题,单例就用上面的方法创建:
Config &Config::i(){ static Config cfg(); return cfg; } Config::Config() : QObject(nullptr){ m_configFile0 = new QSettings("c:\\abc.ini",QSettings::IniFormat); // 解决中文乱码问题 m_configFile0->setIniCodec(QTextCodec::codecForName("UTF-8")); }
看到没,这个扛精他把文件写到构造函数类里面,文件为c:\\abc.ini。
也就是说,他这个Config类,移植到别的工程的时候,都要改一下这个文件名,才能起一个新名字。当然他也可以起一个唯一的跟程序绑定名字,目录也可以放到temp,这又是牛角尖,我们不说。
假如他是负责这个模块,然后这个模块,比如dll,要给别的exe工程用,那是不是每个exe都要一个不同的dll?他还没发现他在做重复工作是不是?svn一大堆重复的Config工程?
那我们变通下:
class Config : public QObject{ public: // 单实例,增加参数 static Config& i(QString file=""); void save(const QString &key,const QVariant &value); };
Config &Config::i(QString file){ static Config cfg(file); return cfg; } // 构造时增加参数 Config::Config(QString file) : QObject(nullptr){ m_configFile0 = new QSettings(file,QSettings::IniFormat); // 解决中文乱码问题 m_configFile0->setIniCodec(QTextCodec::codecForName("UTF-8")); }
如上所述,这个dll首次构造时是可以带一个参数进去的,就是可以把配置文件路径传进去,后续访问是不需要的。
这样,这个dll只需要维护一份,就可以配形形色色的exe了。
说了这么多,显然是证明这个扛精,代码写的不少,却没有得到精华,行将就木在copy patse,不会灵活变通。
面试过30多岁的人,写的代码,我都不忍直视,真是一点水平都没有。
一堆公众号,还说要30岁程序员要转型,bla bla bla,我转你妹,写的这么差,好意思跟人家国外50岁的程序员PK吗?能做出好的产品吗?
心浮气躁,不懂变通,一定是做不出好产品的!
博主爸爸:你这沙雕真搞笑,我写了几十万行代码都没写过构造函数加参数的单例,都是单例了不会把参数写进类里面么
2021-10-04 12:26:36 回复
空名网友:回收的问题不复杂,我的举例只是为了讲请单例模式没有详细说明,可以借助shared_ptr在程序结束时自动完成资源回收,call_once的例子改进如下:std::shared_ptr<Singleton> Singleton::m_instance;std::shared_ptr<Singleton> Singleton::getInstance() { // 用于call_once的局部静态变量 static std::once_flag oc; std::call_once(oc, [&] { m_instance = std::make_shared<Singleton>();}); return m_instance;}
2021-09-03 10:20:28 回复