C++单例模式及线程安全(含2004年和2011年版本)

老鸟

一般用 C++ and the Perils of Double-Checked Locking这篇文章,老外写的,加了注释如下:

C++ and the Perils of Double-Checked Locking.pdf

比较沙雕的翻译如下:

C++和双重检查锁定模式(DCLP)的风险.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


static创建

有的沙雕说,我直接定义一个静态的返回不就行了嘛?我多聪明啊,而且还有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吗?能做出好的产品吗?

心浮气躁,不懂变通,一定是做不出好产品的!

本文为3YL原创,转载无需联系,但请注明来自labisart.com。

原创文章不易,如果觉得有帮助,可打赏或点击右侧广告支持:

查看打赏记录

发表评论请遵守党国法律!后台审核后方可显示!
  • 最新评论
  • 总共2条评论

博主爸爸:你这沙雕真搞笑,我写了几十万行代码都没写过构造函数加参数的单例,都是单例了不会把参数写进类里面么

2021-10-04 12:26:36 回复

  • 3YL 回复 博主爸爸:其实这个沙雕不说是不是写了几十万行,就说后面,哪里说了单例就不能传参数了???你这不是让C++ and the Perils of Double-Checked Locking.pdf这几个兄弟白研究了吗。所以说,经验丰富不可怕,可怕的是目中无人。
  • 2021-10-05 08:22:09 回复

空名网友:回收的问题不复杂,我的举例只是为了讲请单例模式没有详细说明,可以借助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 回复

  • 3YL 回复 空名网友:在h里定义:static std::share_ptr<Singleton> m_instance;我还遇到问题是构造析构函数声明为private不行。使用自定义删除器由于要操作内部成员,又提示非静态调用。
  • 2021-09-28 17:43:25 回复
  • Blog v1.1© 2022 labisart.com 版权所有 | 联系:labartwork@163.com