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的标记已经被改变。
参考文献