C++并发与多线程学习笔记--单例设计模式、共享数据分析

  • 设计模式
  • 共享数据分析
  • call_once

 


 

设计模式

开发程序中的一些特殊写法,这些写法和常规写法不一样,但是程序灵活,维护起来方便,别人接管起来,阅读代码的时候都会很痛苦。用设计模式理念写出来的代码很晦涩,国内的05~10年的时候有一本“Head First”,写程序的时候谈到设计模式。

项目开发经验+模块开发经验=设计模式

先有开发需求,然后把一个大的工程拆分很很多小的模块,然后演变出设计模式。当设计模式传到国内来的时候,很多程序员写代码把设计模式往代码中套,使得一个很小的程序的变得复杂,而且源码晦涩,本来设计模式是为了把大的东西拆成小的东西,编程的时候,各个模块方便管理。而不是生搬硬套,写程序的时候要“活学活用”。

单例设计模式,使用的频率比较高:在整个项目中,有某个或者某些特殊的类,属于该类的对象,我只能创建一个,多了创建不了。单例是写法比较特殊的类,整个项目中只能有一个,用于配置文件之类的操作。

单例类;

class MyCAS //这是一个单例类
{
private:
    MyCAS(){}           //构造函数私有化了
                               //不能创建对象
private:
    static MyCAS *m_instance; //静态成员变量

public:
    static MyCAS *GetInstance()
    {
         if(m_instance ==nullptr)
             { 
                 m_instance = new MyCAS();
              } 
          return m_instance;
    }
};

 在main函数之外,类静态变量初始化

MyCAS *MyCAS::m_instance = nullptr;     //静态变量的初始化

 在main函数内

MyCAS *p_a = MyCAS::GetInstance(); //创建一个对象,返回该类对象的指针

    如果再写 MyCAS *p_b = MyCAS::GetInstance(),此时仍然指向同一个实例,返回唯一的对象指针。

析构函数写法,此时还有析构代码要写上,在原来的类中定一个新类,类中套类,用来释放对象。

public:
    static MyCAS *GetInstance()
    {
         if(m_instance ==nullptr)
             { 
                 m_instance = new MyCAS();
                 static  GarRelease cl;                   //程序退出的时候析构这个类,自动释放资源
              } 
          return m_instance;
    }

   class GarRelease
   {
          ~GarRelease()
          {
              if(MyCAS::m_instance)
             { 
                 delete MyCAS::m_instance;
                 MyCAS::m_instance = nullptr;
             }
          }
    };

  小结:因为把构造函数私有化了,不能通过对象直接生成对象,只能通过一个static来生成,并且构造指针指向同一个对象,整个类的写法就是一种设计模式。

共享数据分析

单例模式会面临一个问题,GetInstance被多个线程使用,如果一个数据是只读的,多个线程之间不需要互斥,但是有一个问题,单例对象创建的时候,在创建之前把对象初始化,载入数据,后续只读。

实际中可能面临的问题:需要我们在自己创建的线程中创建单例类,这种线程不止一个。

可能需要面临GetInstance()需要互斥的情况:

如果两个线程用同一个入口函数:

void myThread()
{
    MyCAS *p_a = MyCAS::GetInstance();
}

  会出现问题:两个线程是同一个入口函数,但是这个两个线程,不管用的是哪个入口函数,两个线程意味着两个流程(通路),同时开始执行这个函数,此时需要一个互斥量防止多个线程同时调用GetInstance()函数

std::unique_lock<std::mutex> mymutex(resource_mutex); //自动加锁,出了作用域之外自动解锁

  程序写完,还是会被怼,程序写完里面有很多地方都要调用GetInstane(),拿到对象指针,如果调用程序很频繁使用,效率非常低。在最外面一层包裹一个判断条件,如果:

a)if(m_instance!=nullptr) 条件成立,则肯定表示m_instance 已经被new过了;

b)if(m_instance ==nullptr) 条件成立,不代表m_instance 一定没被new过;

c)双重锁定;

    static MyCAS *GetInstance()
    {
         if(m_instance ==nullptr)
         { 
            std::unique_lock<std::mutex> mymutex(source_mutex)
            if(m_instance ==nullptr)
             { 
                 m_instance = new MyCAS();
                 static  GarRelease cl;                  
              }                
          } 
          return m_instance;
    }

  

call_once

std::call_once()是一个函数模板,这个也是C++11引入的函数,其中第二个参数,就是一个函数名,第二个参数就是一个函数名a(),call_once()能够保证函数a只被调用一次,比如说有两个线程,都调用了函数a,正常情况下被调用了两次,如果用call_once,就能保证函数只被调用了一次,如果把核心的共享数据代码,(new 一个对象)。

单例对象在多线程的情况下,初始化需要mutex,call_once具备互斥量这种能力,而且效率上比互斥量消耗的资源更少。call_once需要与一个标记结合使用,std::once_flag,其实这是一个结构,就可以看成一个标记。通过这个标记来决定对应的函数是否执行,调用call_once成功后,call_once就把这个标记设置为已调用的状态,后续再次调用call_once(),只要标记被设置为“已调用”状态,那么对应的函数a就不会在被执行了。

std::once_flag g_flag;   //这是个系统定义的标记

 完整的写法

class MyCAS
{
    static void CreateInstance(); //只被调用一次的函数
     {
          m_instance = new MyCAS();
          static GarRelease cl;
     }
     static MyCAS *GetInstance()
    {

          std::call_once(g_flag, CreateInstance);
          return m_instance;
    }

}

  假设两个线程都同时开始执行GetInstance(),同时执行到std::call_once(),call_once就好像一个锁,其中一个线程调用,另一个线程就要等当前这个线程执行完毕,才会去决定是否调用CreateInstance(),此时的call_once的标记已经被改变。

 

参考文献

https://study.163.com/course/courseLearn.htm?courseId=1006067356#/learn/video?lessonId=1053491360&courseId=1006067356

posted @ 2019-11-10 12:34  adminmttt  阅读(583)  评论(0编辑  收藏  举报