单例九品第七品
- 上一品引入
- 写在前边
- 代码部分
- 实现思路的评注与思考
- 下一品的设计思考
上一品引入
第六品着重解决了因为链接顺序造成的未定义问题,通过强制对象完成编译期初始化和使用基本类型代替抽象类型,使得全局对象的缺省初始化从不平凡变为平凡初始化,从而解决了因翻译单元链接顺序不同造成的未定义问题。但是,因为初始化子类中的全局对象构造需要计数值的配合,正是计数值这种写法造成了多线程的安全问题,第六品的例子3可能会出现单例对象的过早销毁,从而因此程序崩溃的问题。
所以,第七品将会着重解决这个问题。
另外从第五品开始引入了指针,全局对象都变成了指针类型,第五品是智能指针和智能指针的拓展类,第六品为裸指针。指针与引用的区别就是,引用完成了对象的绑定就不会在改变绑定对象,就可以理解为是一个有固定指向的指针。但是指针的话,如果没有限定指向,是可以自由变更朝向的,这个问题就是第五品以后,将全局对象变为指针后引入的问题。但是这个问题不在本品中进行讨论,指针类型的全局对象存在被修改的问题,将在第八品中进行讨论。第七品只讨论第六品引入的线程安全问题。
写在前边
- 基本思路
• 为引用计数引入线程安全操作 - 优点
• 全局对象初始化/销毁多线程安全 - 缺点
• 指针有被修改的风险
代码部分
三个文件: sing.cpp main.cpp和sing.h
- main.cpp
#include "sing.h"
static Sing::Init init;
auto singletonInst2 = singletonInst->val;int main(int argc, char** argv)
{std::cout << "get value: " << singletonInst2 << '\n';std::cout << singletonInst << std::endl;std::cout << singletonInst->val << std::endl;
}
- sing.cpp
#include "sing.h"
#include <memory>
#include <iostream>Sing* singletonInst; // 全局对象,但是是指针类型的对象,在main函数中可能会被修改,造成程序崩溃Sing::Init::Init()
{auto& count = Sing::Init::RefCount();。auto ori = count.fetch_add(1);//返回的ori为count加1前的值if (ori == 0){singletonInst = new Sing(); // 全局对象访问点}
}Sing::Init::~Init()
{auto& count = Sing::Init::RefCount();auto ori = count.fetch_sub(1);//返回的ori为count减1前的值if (ori == 1){delete singletonInst;singletonInst = nullptr;}
}
- sing.h
#pragma once
#include <atomic>
#include <iostream>class Sing
{
public:struct Init{Init();Init(const Init&) = delete;Init& operator= (const Init&) = delete;~Init();static std::atomic<unsigned>& RefCount(){static std::atomic<unsigned> singletonCount{ 0 };return singletonCount;}};private:Sing(){std::cout << "Sing construct\n";val = 100;}~Sing(){std::cout << "Sing destroy\n";}Sing(const Sing&) = delete;Sing& operator= (const Sing&) = delete;
public:int val;
};extern Sing* singletonInst;
- output
g++ -c main.cpp
g++ -c sing.cpp
g++ main.o sing.o -o ./ms
g++ sing.o main.o -o ./sm
两种链接顺序的结果都是
Sing construct
get value: 100
0x557949424eb0
100
Sing destroy
实现思路的评注与思考
-
这种实现方式使用静态初始化函数的方式完成std::atomic类型的引用计数值的构建,使用静态初始化函数的方式完成计数值singletonCount的初始化。
-
为什么使用std::atmoic完成singletonCount的定义?
答: 1) std::amotic是个原子操作,用于实现原子操作。原子操作是在并发编程中用来确保多个线程在同一时间对共享数据的访问是安全的,不会发生竞态条件(race condition)的操作。2) 在多线程编程中,如果多个线程同时访问同一块内存区域,并且其中至少有一个线程对该内存区域进行写操作,就可能导致竞态条件。std::atomic 提供了一种机制来避免竞态条件,能够确保在单个原子操作中对共享变量进行读取、修改、写入等操作,从而保证这些操作的完整性。 -
使用静态初始化函数的方式完成计数值的初始化,同样会引入多线程安全的时间损失。如果在main函数中多个线程都调用RefCount函数(如果是第一次,就构建singletonCount对象。随后每次掉用这个函数,都不会构建新的,这是静态初始化的作用),那么每次都需要加锁,判断,解锁操作
-
==为什么使用sing类中的静态函数完成计数值singletonCount的定义,在之后每一次使用计数值的时候,再去调用接口Recount呐?这样也会引起多线程的时间消耗,就相当于构建了一个计数值到单例。这么设计也是从程序安全性和限制用户构建sing类的多个不同对象来设计的。具体怎么解释还没想好。待后续补充
- 缺点
全局对象是指针类型的对象,在main函数中可能会被修改,造成程序崩溃。全局对象改为指针类型,是在第四品的例子2到第五品的转换,为了解决第四品的例子2中因为翻译单元链接顺序导致的静态实例初始化灾难,从而在第五品中引入了指针类型的全局对象,并在初始化的时候控制对象的初始化,从而避免链接问题造成的未定义问题 [singletonInst.reset( new Sing)]。为了方便修改指针的指向
下一品的设计思考
第八品将解决全局对象为指针而引入的指针会被修改的风险