一个cpper眼中的singleton

2024-04-11 00:58
文章标签 singleton 眼中 cpper

本文主要是介绍一个cpper眼中的singleton,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在众多的设计模式中,singleton(单件或者单例)绝对是另类的一个。在实现单件的过程中,我深深体会到了“细节是魔鬼”的道理。在看似一览无遗的湖面下,尼斯湖水怪的阴影却屡驱不散。

初识单件是在GoF的设计模式经典著作里面。GoF给出的定义很简单:让一个类只有一个实例,并为实例提供一个全局访问点。而且类图里面只有一个类。太简单了,顿时有种手到擒来的飘飘然。现在回想起来,当时学习设计模式有问题,太注重类图和实现,反倒把应用场景和优缺点忽略了。结果走了不少弯路。

于是,真到要用的时候,赶紧祭出单件(代码如下),还颇引以为傲的介绍为什么Instance要返回引用,而不是指针。引用可以告诫使用者这个对象不应该删除,也不应该保存起来。不过,不应该跟不能还是有区别的,可以把引用转成指针后,删除或者保存。

class Singleton
{
private:Singleton();~Singleton();Singleton(const Singleton & rhs);Singleton & operator=(const Singleton & rhs);
public:static Singleton & Instance(){if (<span style="font-family: Arial, Helvetica, sans-serif;">m_Instance == NULL</span><span style="font-family: Arial, Helvetica, sans-serif;">)</span>
{m_Instance = new Singleton();}return m_Instance;}private:static Singleton * m_Instance;
public:void OtherMethod();
};

乍一看,有点被漂亮的OO方法闪花了眼。讨厌的全局变量用OO包裹了起来,变成了一个类的私有静态变量。别人看不到它,只能通过该类的公有静态方法访问。这又一次证明了软件的所有问题都可以用增加中间层解决。但是,没高兴一会儿,尼斯湖水怪就浮出水面了。

首先,多线程下是不安全的,有内存漏泄的风险。当m_Instance还没new时,线程A先调用Singleton::Instance(),正要new时,切换到线程B,线程B发现m_Instance是空的,索性就new了一个。切回到线程A后,线程A又多new了一个。这是典型的竞争条件,闪过脑海的第一个方法就是加锁。

<span style="font-family: Arial, Helvetica, sans-serif;">Singleton</span><span style="font-family: Arial, Helvetica, sans-serif;"> & </span><span style="font-family: Arial, Helvetica, sans-serif;">Singleton::</span><span style="font-family: Arial, Helvetica, sans-serif;">Instance()</span>
{
<span style="white-space:pre">	</span>Lock guard; // Lock类构造时take信号量,析构时give信号量
<span style="white-space:pre">	</span>if (<span style="font-family: Arial, Helvetica, sans-serif;">m_Instance == NULL</span><span style="font-family: Arial, Helvetica, sans-serif;">)</span>
<span style="white-space:pre">	</span>{
<span style="white-space:pre">		</span>m_Instance = new Singleton();
<span style="white-space:pre">	</span>}
<span style="white-space:pre">	</span>return m_Instance;
}
但是这招杀伤力太大,当m_Instance是空的时候,还行。当m_Instance已经new出来了,还加锁就得不偿失了。于是,坊间又流传出双重检测锁定模式(Double Check Locking Pattern):

<span style="font-family: Arial, Helvetica, sans-serif;">Singleton</span><span style="font-family: Arial, Helvetica, sans-serif;"> & </span><span style="font-family: Arial, Helvetica, sans-serif;">Singleton::</span><span style="font-family: Arial, Helvetica, sans-serif;">Instance()</span>
{if (m_Instance == NULL) // 第一次检测{Lock guard;if (m_Instance == NULL) // 第二次检测{m_Instance = new Singleton();}}return m_Instance;
}
第一次检测是粗略的检测,这可以排除掉m_Instance已经new的情况,此时就不用加锁了。第二次检测是精确的检测,这次是发生在m_Instance还是空的情况下,这就要加锁互斥了。不过, Andrei Alexandrescu在《Modern C++ Design》中指出因为RISC指令重排,这也不一定是线程安全的,所以除了在m_Instance前加上volatile之外,还要加上第三次检测,查阅编译器手册看看怎么消除这种情况。

有些复杂,对吧?再回头想想,Instance()把初始化和运行时查询放在了一起,有点不符合单一职责原则。如果把这两个处理拆分开,就不会有问题了。可以把m_Instance定义成Singleton的静态对象,而不是指针,就不需要new了。但是C++中静态对象有两种:非局部静态变量(全局变量/类静态变量)和局部静态变量,你还得选(知道杨朱为啥泣岐了吧)。两者的实现方式不同:前者在类定义中把m_Instance改成静态对象:

	static Singleton m_Instance;
后者则干脆不要成员变量,而把m_Instance放在Instance()函数中:

<span style="font-family: Arial, Helvetica, sans-serif;">Singleton</span><span style="font-family: Arial, Helvetica, sans-serif;"> & </span><span style="font-family: Arial, Helvetica, sans-serif;">Singleton::</span><span style="font-family: Arial, Helvetica, sans-serif;">Instance()</span>
{static Singleton s_Instane;return s_Instance;
}
两者的初始化时机不同:前者是在main()函数执行之前初始化的,由于不同编译单元的非局部静态变量的初始化顺序未定义,如果涉及静态变量之间的依赖,这个方案不可行。

后者是第一次调用Instance()时初始化,伪代码如下:

Singleton & Singleton::Instance()
{
<span style="white-space:pre">	</span>extern void __DestroySingleton();
<span style="white-space:pre">	</span>static bool __is_instance_inited = false;
<span style="white-space:pre">	</span>static char __buffer[sizeof(Singleton)];
<span style="white-space:pre">	</span>if (!__is_instance_inited)
<span style="white-space:pre">	</span>{
<span style="white-space:pre">		</span>new(__buffer) Singleton;  // placement new
<span style="white-space:pre">		</span>__is_instance_inited = true;
<span style="white-space:pre">		</span>atexit(__DestroySingleton);
<span style="white-space:pre">	</span>}
<span style="white-space:pre">	</span>return *reinterpret_cast<Singleton *>(__buffer);
}
void __DestroySingleton()
{
<span style="white-space:pre">	</span>reinterpret_cast<Singleton *>(__buffer)->~Singleton();
}
后者相当于把判断s_Instance是否构造的判断留给编译器实现,所以可能会出现多个线程调用多次构造函数的情况,因为用的是placement new,不会new多个对象。

除了把指针改成对象,还可以在成员函数上做文章,比如可以再定义一个全局变量来强制Instance()函数在初始化时调用:

Singleton & force_init = Singleton::Instance();
又比如,专门增加一个初始化接口:

void Singleton::Create()
{Lock guard;if (m_Instance == NULL){m_Instance = new Singleton;}
}
static Singleton & Instance()
{return m_Instance;
}
初始化一般是在单线程环境下完成的,所以Create()甚至可以把锁去掉。

说了这么多,到底应该用哪个方案呢?答曰:依实际场景而定。如果没有静态变量的依赖,构造开销很小,而且生命周期无限,可以用静态成员对象。
另一个让我头疼的问题是单件代码的复用。



收益能超过成本吗?

单件的优点如下:

1、对类实例化的控制。能确保只有一个实例。这点全局变量做不到,但是,既然有一个全局变量,谁还会去new一个新的对象呢?

2、延迟求值。这需要一点技巧。上面的代码就是一个很好的例子,把实例化推迟到第一次调用Instance()的地方。但并不是所有单件都能延迟实例化,比如,下面这样实现就不能延迟求值。

class Singleton
{
public:static Singleton & Instance(){return m_Singleton;}private:static Singleton m_Singleton;
};

全局变量能不能做到呢?可以,但不是很完美,因为不知道第一次访问全局变量的确切时间,可能出现访问全局变量的时候,全局变量还没有初始化。

3、对类的初始化顺序的控制。毕竟把全局变量的访问点封闭到函数里面了,在函数体内部可以做任何事件,比如把依赖的对象都给初始化了,再初始化自己。全局变量对此爱莫能助。

4、提供优雅的访问接口。至少从形式上,看上去很符合OO的要求。

5、内聚比较高。如果是全局变量,初始化和访问点是分离的。Bob大叔在《Clean Code》里面提到类的内聚性的一个衡量标准:类的成员变量被越多的类方法访问,类的内聚性就越高。从这一点看,单件的内聚性不是很高,因为m_Singleton只被Instance()访问。


单件的缺点:

1、很难抽象化。其它的设计模式,比如观察者模式,只需要定义一个接口类,然后以此为起点,派生出实现类就行了。如果定义一个Singleton的抽象类S,然后把想到单件化的类D继承自S,类D是不是单件呢?显然不是。但是在《Modern C++ Design》一书中,Andrei Alexandrescu介绍了用模板实现单件基类,然后把具体类的类名,构造方法和生命周期管理方法通过模板参数引入模板基类中。他用了整整一章的篇幅详细的介绍单件,说明什么?单件的技巧性很高,有点像拿着杆子走钢丝。模板化的单件有一个限制,代码共享只能基于源代码级别的共享,而不能基于二进制级别。于是模板化的单件更多的是模块内部使用,很少作为DLL的接口。

2、很难继承,Singleton的子类很难也是Singleton。或者说技巧性也很高。首先要把父类的构造函数改为protected,允许子类调用。这样父类其实就不是单件了。而且会产生反向依赖,即父类要看到子类,在C++中,由于protected方法的单向性(父类不能访问子类的protected方法),父类还必须是子类的友元。

3、违反了Single Responsibility原则。有人甚至认为单件是反模式。我觉得单件从某种程度上,违反了基于接口编程。

4、效率问题。在多线程环境中,尽管有double checked技术,但是每次Instance调用还是有至少一次的if判断。

5、析构问题。这个问题跟使用全局变量一样,你永远不知道有谁还抓着Singleton引用不放。当然可以加上引用计数,但是你不觉得太复杂了吗?


说了这么多,到底该不该单件呢?这是菜鸟喜欢问的问题。高手一般这么问:应该在什么场景下使用单件呢?知道差距了吧。没有一个设计模式是放之四海皆准的,不然也不会有这么多的设计模式了。存在即是合理的。单件也有它特定的应用场景。就像辣椒酱一样,你不会所有菜里面都会放吧。

我觉得单件应该受限使用,毕竟单件说白了,也是访问全局变量。如果在代码中大量使用单件,也就是说,大量使用全局变量。这是一种设计的坏味道。这时候,你需要回过头来,想想设计有没有问题?类的职责划分有没有问题?没有吗?真的没有吗?再想想。

那么,单件的替代方案是什么呢?我想有两个:

1、MONOSTATE模式。把类的所有成员变量定义成静态成员变量,不也就是单件了吗?有趣吧。这个模式最有趣的地方是访问者根本不知道MONOSTATE类是单件。

2、依赖倒置,也有人叫依赖注入。比如,A类要访问B类的接口,在A类中定义一个B类的对象指针和一个Set接口,然后由第三方的工厂对象把B类的实例(或者是B的子类的实例)构造出来,再通过A类的Set接口,把B类的实例注入到A类中。


这篇关于一个cpper眼中的singleton的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/892648

相关文章

单例模式singleton

此篇为学习笔记,原文链接 https://refactoringguru.cn/design-patterns/singleton 保证一个类只有一个实例, 并提供一个访问该实例的全局节点。 隐藏构造函数并实现一个静态的构建方法

剑指offer-面试题2.实例Singleton模式

题目:设计一个类,我们只能生成该类的一个实例 这道题显然是对设计模式的考察,很明显是单例模式。什么是单例模式呢,就是就像题目所说的只能生成一个类的实例。那么我们不难考虑到下面几点: 1.不能new多个对象,那么必然该类的构造函数是私有的 2.类对象只有一个,那么必然该对象只能有一个私有的静态成员变量,该成员变量为类实例或者类实例的指针。 3.但是我们同时还要考虑到如果获取

一个电商创业者眼中的618:平台大变局

战役结束了,战斗还在继续。 一位朋友去年5月创业,网上卖咖啡,这个赛道很拥挤,时机也不好,今年是他参加第一个618。朋友说,今年的目标是锤炼团队,总结方法,以及最重要的——活下去。 朋友最早在湖南广电,下海开过广告公司,做过咖啡馆,也做过餐饮,年近不惑,财务自由,平时就是收收租,踢踢球,也很无聊。看到长沙日新月异的新消费潮流,茶颜悦色、柠季、零食很忙、三顿半、果呀呀等,雨后春笋一样涌现,他觉得

设计模式之单例模式-Singleton

Singleton单类模式是最简单的设计模式,它的主要作用是保证在程序运行生命周期中,使用了单类模式的类只能有一个实例对象存在。单类模式实现了类似C语言中全局变量的功能,单类模式常用于注册/查找的服务。 单类模式的UML图如下: 单类模式有两种实现方式:饱汉模式和饿汉模式,如下: 1.饱汉单类模式例子代码: [java] view plain copy public class S

设计模式--创建型-Singleton(单例单件)

设计模式--创建型-Singleton(单例/单件)       1. 意图   保证一个类仅有一个实例,并提供一个访问它的全局访问点。       2. 结构图           3. 简述   通常需要满足以下两点:   3.1   要保证类只能实例话一次,最简单的方法是把构造(包括拷贝构造函数和赋值构造函数)全部设为private或pr

Mybatis异常There is no getter for property named或Returning cached instance of singleton bean

mapper接口中的参数需要加上@Param(value="xxx"),如:getComFairList(@Param(value = "comCode") String comCode)

(C++实现)——单例模式(Singleton Pattern)

单例模式,顾名思义,就是只能由一个实例,那么我们就必须保证 该类不能被复制。该类不能被公开的创造。 那么对于C++来说,他的构造函数,拷贝构造函数和他的赋值函数都不能被公开调用。 但对于该私有的构造函数的构造时机上来说也可以分两种情况来构造:  只有当需要改类的时候去构造(即为懒汉模式) 在程序开始之前我就先构造好,你到时候直接用就可(即为饿汉模式)

spring框架的singleton和prototype在高并发的表现

spring的controller、service、dao都是默认singleton的,在singleton的模式下spring只会生成一个对象来处理并发的请求,例如: @Controller@RequestMapping("test")public class Test {private int num = 0;@RequestMapping("test")@ResponseBodyp

bean作用域为singleton(单例模式)引起多线程安全问题

华为云OBS整合了Ueditor,但是在批量上传图片时,只能部分上传成功,很多文件会上传失败,经过分析发现:bean作用域为单例模式时,Spring IoC 容器中只会存在一个共享的 Bean 实例,无论有多少个Bean 引用它,始终指向同一对象,该模式在多线程下是不安全的,特此记录。 错误代码: @Service@Slf4jpublic class FileServiceImpl imp

(P17)muduo_base库源码分析:线程安全Singleton类实现

文章目录 1.线程安全Singleton类实现 1.线程安全Singleton类实现 线程安全Singleton类实现 pthread_once atexit typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1]; 类图 +号表示公有的,-号表示私有的。使用模板方式实现 eg:src\17\jmudu