17.7 C++并发与多线程-单例设计模式共享数据分析、解决与call_once

17.1 C++并发与多线程-基础概念与实现
17.2 C++并发与多线程-线程启动、结束与创建线程写法
17.3 C++并发与多线程-线程传参详解、detach坑与成员函数作为线程函数
17.4 C++并发与多线程-创建多个线程、数据共享问题分析与案例代码
17.5 C++并发与多线程-互斥量的概念、用法、死锁演示与解决详解
17.6 C++并发与多线程-unique_lock详解
17.7 C++并发与多线程-单例设计模式共享数据分析、解决与call_once
17.8 C++并发与多线程-condition_variable、wait、notify_one与notify_all
17.9 C++并发与多线程-async、future、packaged_task与promise
17.10 C++并发与多线程-future其他成员函数、shared_future与atomic
17.11 C++并发与多线程-Windows临界区与其他各种mutex互斥量
17.12 C++并发与多线程-补充知识、线程池浅谈、数量谈与总结

7.单例设计模式共享数据分析、解决与call_once

  7.1 设计模式简单谈

    设计模式,其实是国外的开发者应付特别大的项目时把项目的开发经验、模块划分经验等总结起来构成的一系列开发技巧(先有开发需求,后有理论总结和整理)。不过这件事情拿到国内来就有点不太一样了,很多人拿着程序硬往设计模式上套,一个小小的项目非要用几个设计模式进去,本末倒置,与推出设计模式的初衷完全相反。
    设计模式有它独特的优点,但读者如果接触到设计模式,还是应该活学活用,不要深陷其中,生搬硬套,笔者认为这对程序员的成长弊大于利。

  7.2 单例设计模式

    其中有一种设计模式叫单例模式,使用频率比较高。什么叫单例呢?就是整个项目有某个或者某些特殊的类,属于该类的对象,只能创建一个,无法创建多个。

class MyCAS  //这是一个单例类
{
private:
    MyCAS() {} //构造函数是私有的

private:
    static MyCAS* m_instance;
public:
static MyCAS* GetInstance()
{
    if (m_instance == NULL)
    {
        m_instance = new MyCAS();
        static CGarhuishou cl;//生命周期一直到程序退出
    }
    return m_instance;
}
class CGarhuishou  //类中套类,用于释放对象
{
public:
    ~CGarhuishou()
    {
        if (MyCAS::m_instance)
        {
            delete MyCAS::m_instance;
            MyCAS::m_instance = NULL;
        }
    }
};
    void func() //普通成员函数,方便做一些测试调用
    {
        cout << "测试" << endl;
    }
};
{
    MyCAS* p_a = MyCAS::GetInstance(); //创建单例类MyCAS类的对象
    p_a->func();  //一条测试语句,用于打印结果
    MyCAS::GetInstance()->func(); //这种写法的测试语句也可以打印结果
}

    这里简单说一下该单例类对象在程序运行结束时的释放原理。因为cl是一个静态成员变量,其生命周期会一直持续到整个程序的退出,当整个程序退出的时候,会调用cl所属类(CGarhuishou)的析构函数,在该析构函数中释放该单例类对象的内存。

  7.3 单例设计模式共享数据问题分析、解决

    接下来可能要面临一个问题,就是这个单例类可能会被多个线程使用,如果能够做到这个单例类中的数据被初始化完之后是只读的,那么不要紧,只读数据是可以被多个线程同时读的,不需要互斥。
    在上面的代码中,这个单例类对象的创建是在主线程中完成的,这没有什么问题,而且在主线程中并在所有其他子线程创建并运行之前创建MyCAS单例类对象的做法是强烈推荐的。因为这个时候不存在多线程对这个单例类对象访问的冲突问题。如果这个单例类对象还需要从配置文件中装载数据,那么也可以在这个时机把所有该装载的文件数据都装载进来。这样如果以后在所有其他线程中都只需要从这个单例类中读共享数据,那么在多个线程中访问(读)这些共享数据都不需要加锁,可以随意随时读。
    但是笔者并不排除,在实际项目中可能会面临着需要在程序员自己创建的线程(而不是主线程)中创建MyCAS单例类对象,而且程序员自己创建的线程可能还不是1个,而是至少2个,也就是说,这段创建MyCAS单例类对象的代码(GetInstance)可能需要做互斥。看看如下这种写法。

static MyCAS* GetInstance()
{
    if (m_instance == NULL)
    {
        std::unique_lock<std::mutex> mymutex(resource_mutex); //自动加锁
        if (m_instance == NULL)
        {
            m_instance = new MyCAS();
            static CGarhuishou cl;//生命周期一直到程序退出
        }			
    }
    return m_instance;
}

void mythread()
{
    cout << "我的线程开始执行了" << endl;
    MyCAS* p_a = MyCAS::GetInstance(); //在这里初始化就很可能出现问题
    cout << "我的线程执行完毕了" << endl;
    return;
}
{
    std::thread mytobj1(mythread);
    std::thread mytobj2(mythread);
    mytobj1.join();
    mytobj2.join();
}

    可以注意到,在上面多包了一层if(m_instance==NULL),也就是有两个if(m_instance==NULL),许多资料上叫这种写法为“双重锁定”或者“双重检查”。
第一次看到这种代码的读者可能不太习惯,也不太理解为什么要多包一层if(m_instance==NULL),其实就是为了提高效率而采用的一种代码书写手段。笔者解释一下:
    (1)必须要承认一点:如果条件if(m_instance!=NULL)成立,则肯定代表m_instance已经被new过了。
    (2)如果条件if(m_instance==NULL)成立,不代表m_instance一定没被new过,因为很可能线程1刚要执行“m_instance=newMyCAS();”代码行,就切换到线程2去了(结果线程2可能就会new这个单例对象),但是一会切换回线程1时,线程1会立即执行“m_instance=newMyCAS();”来new这个单例对象(结果new了两次这个单例对象)。
    所以笔者才会说:if(m_instance==NULL)成立,不代表m_instance一定没被new过。
那么,在m_instance可能被new过(也可能没被new过)的情况下,再去加锁。加锁后,只要这个锁能锁住,那再次判断条件if(m_instance==NULL),如果这个条件依然满足的话,那肯定表示这个单例类对象还没有被初始化,这个时候就可以放心地用new来初始化。
    例如,线程1马上要执行代码行“m_instance=newMyCAS();”时,一下切换到线程2去了,那线程2拿不到锁它就要卡在那里(此时又会自动切换回线程1),等线程1执行完new的操作,然后释放了锁,线程2拿到锁,此时线程再判断if(m_instance==NULL)条件肯定就不成立了。
    那平时常规调用GetInstance的时候,因为最外面有一个条件判断if(m_instance ==NULL)在,这样就不会每次调用GetInstance都会创建一次互斥量。也就是说,平常的调用根本就执行不到创建互斥量的代码,而是直接执行“returnm_instance;”,这样调用者就能够直接拿到这个单例类的对象,所以肯定提高了执行GetInstance的效率。

  7.4 std::call_once

    这里借着刚才所讲述的案例,顺便简单讲解一下call_once的用法。这是一个C++11引入的函数,这个函数的第二个参数是某个其他的函数名。
    假设有个函数,名字为a,call_once的功能就是能够保证函数a只被调用一次。读者都知道,例如有两个线程都调用函数a,那么这个函数a肯定是会被调用两次。但是,有了call_once,就能保证,即便是在多线程下,这个函数a也只会被调用一次。请读者想象,如果把刚才的单例类对象的初始化代码放到这种只被调用一次的函数a里,是不是也能解决刚才所面对的单例对象在多线程情况下初始化需要互斥的问题。
    所以从这个角度讲,call_once也是具备互斥量的能力的,而且效率上据说比互斥量消耗的资源更少。
    引入std::once_flag,这是一个结构,在这里就理解为一个标记即可,call_once就是通过这个标记来决定对应的函数a是否执行,调用call_once成功后,call_once会反转这个标记的状态,这样再次调用call_once后,对应的函数a就不会再次被执行了。

static MyCAS* GetInstance()
{
    if (m_instance == NULL) //同样为提高效率。
    {
        //cout << std::this_thread::get_id() << endl;
        std::call_once(g_flag, CreateInstance); //两个线程同时执到这里时,其中一个线程卡在这行等另外一个线程的该行执行完毕(所以可以把g_flag看成一把锁)。	
    }
    return m_instance;
}

    通过设置断点、跟踪调试以及在CreateInstance中增加std::this_thread::sleep_for等手段,调试程序可以发现,当两个线程同时执行到代码行std::call_once的时候,只有一个线程真正进入了对CreateInstance的调用中去了,此时,另外一个线程卡在std::call_once所在行处于一直等待中。当进入CreateInstance的线程返回时,g_flag标记被设置,这个设置导致卡在std::call_once行的另一个线程不会再去调用CreateInstance函数,从而保证即便是有多个线程存在,但对CreateInstance函数的调用只有一次,从而确保了单例类对象只会被new一次(“m_instance =newMyCAS();”代码行只会被执行一次)。
    最后值得再次说明的是,虽然本节讲的还是多线程互斥的话题,但是针对这种单例类对象的初始化工作,强烈建议放在主线程中其他子线程创建之前进行,这样当各个子线程开始工作时,单例类对象已经创建完毕,就完全不必要在GetInstance成员函数中考虑多线程调用时的互斥问题,只需要像本节最初那样书写GetInstance版本代码行即可。也就是下面这个版本:

static MyCAS* GetInstance()
{
    if (m_instance == NULL)
    {
        m_instance = new MyCAS();
        static CGarhuishou cl;//生命周期一直到程序退出
    }
    return m_instance;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值