一个偶然的机会,知道了std::once_call这个东西。

了解了下,std::once_call支持多线程情况下的某函数只执行一次。咦,这个不是恰好符合单例模式的多线程安全的困境吗?

单例模式,经常需要手写的经典面试题之一,很考验面试者的底子和水平。需要考虑的细节很多,其中多线程安全也是一个点。

本篇博文再次总结下单例模式,并且尽可能详细与完整,建议mark,面试前再回忆下(毕竟工作中直接有代码可以抄)。

单例模式,在本人看来是全局变量的一种C++封装。

常规的C语言中,经常会在文件开头定义一坨全局变量,有些还加上extern来支持变量的跨文件访问。确实难以维护,而且当项目庞大了,

有可能发生变量被偷偷修改的情况,导致一些奇怪难以排查的bug。

单例模式,则提供了一个供全局访问的类,包含了一系列全局访问的变量与方法,经过组织之后,变量的维护更清晰。一般以管理类居多。

带Manager类名的类往往都是单例模式实现的。

常规的单例模式的设计,仅能通过Instance方法(有些喜欢getInstance)类指针或者得到类实例(往往是引用,毕竟只有1个实例)。因此,

第一点要考虑的就是禁止构造函数、拷贝构造与赋值函数。如果需要释放资源,一般不允许调用delete 方法,最多对外提供Releace(有些喜欢Destroy)

方法来内部释放资源换。因此析构函数也要禁用。通用的单例模式的第一份装逼代码先写好。

#define SINGLETON_CTOR(x) \
private:\
x() = default;\
x(const x&)=delete;\
x& operator=(const x&)=delete;\
~x()=default;

因为不能通过构造函数得到类实例,因此类的Instance方法必须是static(即绑定到类对象设计本身,不属于类实例的方法)的。

有些人会区分饿汉式或者懒汉式的,面试时有时会紧张,写不全代码,先记住最简单又安全的实现。

class Singleton
{
SINGLETON_CTOR(Singleton);
public:
static Singleton& Instance()
{
static Singleton _instance;
return _instance;
}
};

静态局部变量会在第一次使用时初始化,多次调用被会编译器忽略,生命周期是程序的运行区间,并且是多线程安全的。

因为静态局部变量是分配在全局静态数据区(不是堆或者栈),内存一直都在(默认全部填0,但不占程序大小bss段)。

在我看来算属于饿汉式的,即程序运行期间就需要内存。

ok,我们看看其他变体实现。

class Singleton2
{
SINGLETON_CTOR(Singleton2);
public:
static Singleton2* Instance()
{
static Singleton2 _instance;
return &_instance;
}
};

有些人喜欢指针多一点。。。,就返回指针好了。

当然,既然如此简单,我们可以通过宏再加工一下,方便他人使用。

#define SINGLETON_INSTACNCE(x) SINGLETON_CTOR(x)\
public:\
static x* Instance()\
{static x _instance; return &_instance;} class SingletonOnceMore
{
SINGLETON_INSTACNCE(SingletonOnceMore);
public:
void fun(){}
};
class SingletonTwiceMore
{
SINGLETON_INSTACNCE(SingletonTwiceMore);
public:
void fun(){}
}; SingletonOnceMore::Instance()->fun();
SingletonTwiceMore::Instance()->fun();
class Singleton3
{
SINGLETON_CTOR(Singleton3);
public:
static Singleton3* Instance()
{
return &_instance;
} static Singleton3 _instance;
}; Singleton3 Singleton3::_instance; //这个得放cpp中,不然编译报错

静态成员变量也是ok的。

到这里为止,都是饿汉式的实现,接下来实现下懒汉式。

class SingletonNotGood
{
SINGLETON_CTOR(SingletonNotGood);
public:
static SingletonNotGood* Instance()
{
if (!_pInstance)
{
_pInstance = new SingletonNotGood;
}
return _pInstance;
}
static SingletonNotGood* _pInstance;
}; SingletonNotGood* SingletonNotGood::_pInstance;//这个得放cpp中,不然编译报错,静态成员默认赋null。

这是最简单的一种懒汉式实现。即看看指针存在否,不存在new一下。但存在一些问题。

1、内存无法正常释放

2、多线程不安全

尽管存在这些问题,但是如果你的管理类的生命周期与程序一样长,就可以不用考虑内存泄漏,毕竟操作系统会在程序退出时自动回收。(不过小心socket,可能导致不能正常关闭的问题 )

然后如果没有多线程的困扰(比如很多管理类带有Init方法,在main函数的入口不远处先调用Init方法来实例化),那么这个简单的方法项目中还是可以用的。

当然,本文既然是总结,我们还得继续。一种简单的优化后如下:

#include <mutex>

class SingletonNotGood
{
SINGLETON_CTOR(SingletonNotGood);
public:
static SingletonNotGood* Instance()
{
std::lock_guard<std::mutex> lock_(m_cs);
if (!_pInstance)
{
_pInstance = new SingletonNotGood;
}
return _pInstance;
}
static void Release()
{
std::lock_guard<std::mutex> lock_(m_cs);
if (!_pInstance)
{
delete _pInstance;
_pInstance = nullptr;
}
}
private:
static SingletonNotGood* _pInstance;
static std::mutex m_cs;
}; SingletonNotGood* SingletonNotGood::_pInstance;//这个得放cpp中,不然编译报错,静态成员默认赋null。
std::mutex SingletonNotGood::m_cs;//这个得放cpp中,不然编译报错,

这里我们还可以使用 Double-Checked Locking Pattern (DCLP) 来减少锁的竞争机会,因为大部分情况下,_pInstance都是非空的。

static SingletonNotGood* Instance()
{
if (!_pInstance) //读操作1
{
std::lock_guard<std::mutex> lock_(m_cs); //只有空的情况下才加锁
if (!_pInstance)
{
_pInstance = new SingletonNotGood; //写操作2
}
}
return _pInstance;
}

尽管这个术语非常高上大,很多博客也会提及,但其实细究起来,它并不是线程安全的。

注意到_pInstance = new SingletonNotGood,是一个写操作,前面有一个无锁的读操作。当真正的写操作进行时,前面的读操作存在脏读情况。

_pInstance = new SingletonNotGood,表面上一个语句,展开后由

1、malloc 一块内存,地址复制到_pInstance

2、针对_pInstance 地址上调用placement new进行类的构造。

当多线程情况下,一个线程有可能进行了1之后,另外个线程进来后,判断非空,进行类对象的访问,导致crash。

如果这样写的项目没有遇到崩溃,大概率都是在main的某个地方提前实例化过了(如管理类很多有init方法,调用了就实例化了)。

这个崩溃的场景的概率真的很小 ,需要多线程恰好同时调用Instance,并且某一个线程执行了malloc后,分出时间片,另外个线程拿到了未构造的类实例进行操作。

但如果面试过程中,你能指出这一点,也是加分项吧。。。。

好的,优化后的单例looks good了,但还是有内存泄漏的风险,用户确实忘了Release了,有时,也不敢乱Release(因为你不知道还有其他人要弄否)。想要自动管理

内存释放?当然可以的。方法一:加一个垃圾收集类。

class SingletonNotGood
{
SINGLETON_CTOR(SingletonNotGood);
public:
static SingletonNotGood* Instance()
{
if (!_pInstance) //读操作1
{
std::lock_guard<std::mutex> lock_(m_cs); //只有空的情况下才加锁
if (!_pInstance)
{
_pInstance = new SingletonNotGood; //写操作2
}
}
return _pInstance;
}
static void Release()
{
std::lock_guard<std::mutex> lock_(m_cs);
if (!_pInstance)
{
delete _pInstance;
_pInstance = nullptr;
}
} private:
struct GarbageCollect
{
~GarbageCollect()
{
if (!_pInstance)
{
delete _pInstance;
_pInstance = nullptr;
}
}
}; private:
static SingletonNotGood* _pInstance;
static std::mutex m_cs;
static GarbageCollect gc;
}; SingletonNotGood* SingletonNotGood::_pInstance;//这个得放cpp中,不然编译报错,静态成员默认赋null。
std::mutex SingletonNotGood::m_cs;//这个得放cpp中,不然编译报错,
SingletonNotGood::GarbageCollect SingletonNotGood::gc;//这个得放cpp中,不然编译报错,

当然由于静态变量的空间是在全局内存区,其空间的释放是在程序结束才进行释放的。而在程序结束时,系统会自动回收该程序申请的空间。

gc的析构函数释放静态实例时,也是在程序结束时才会调用的。所以这里写的内存释放意义不大。当然对于那些在程序结束后不自动回收空间的系统,还是需要写空间回收的。

方法二,采用智能指针。

#include <memory>
class SingletonUsePtr
{
SINGLETON_CTOR(SingletonUsePtr);
public:
static SingletonUsePtr& Instance()
{
if (!_ptr) //读操作1
{
std::lock_guard<std::mutex> lock_(m_cs); //只有空的情况下才加锁
if (!_ptr)
{
_ptr.reset(new SingletonUsePtr);
}
}
return *_ptr;
}
private:
static std::unique_ptr<SingletonUsePtr> _ptr;
static std::mutex m_cs;
}; std::unique_ptr<SingletonUsePtr> SingletonUsePtr::_ptr;//这个得放cpp中,不然编译报错,
std::mutex SingletonUsePtr::m_cs;//这个得放cpp中,不然编译报错,

这里使用shared_ptr也可以,不过shared_ptr占用的内存和内部复杂度(额外的有个block的概念用于存放引用计数等)稍大点。

推荐返回Singleton & 。用了智能指针就得放弃裸指针。

接下来,终于可以引出std::once_call再次优化以去掉额外的锁了。

class SingletonUsePtr2
{
SINGLETON_CTOR(SingletonUsePtr2);
public:
static SingletonUsePtr2& Instance()
{
static std::once_flag s_flag;
std::call_once(s_flag, [&]() {
_ptr.reset(new SingletonUsePtr2);
}); return *_ptr;
}
private:
static std::unique_ptr<SingletonUsePtr2> _ptr;
};

这个相对来说,是最简单的安全可靠的懒汉式实现了。有兴趣的也可以封装成宏,方便他人使用。

最后,再使用模板实现一份,采用curiously recurring template pattern,CRTP,不详细展开了,您坚持看到现在累了,我也写的累了 = =(主要原因)。

//采用模板再实现一次,
//使用方法 class YourSingleton: public SingletonBase<YourSingleton>
template<typename T> //T 是子类
class SingletonBase
{
SINGLETON_CTOR(SingletonBase); //这个还是可以用的
public:
static T& Instance()
{
static T t; //饿汉式
return t;
}
}; //再加上今天的学习的std::once_call实现懒汉式
template<typename T> //T 是子类
class SingletonBaseLazy
{
SINGLETON_CTOR(SingletonBaseLazy); //这个还是可以用的
public:
static T& Instance()
{
static std::once_flag flag;
std::call_once(flag, [&](){_ptr.reset(new T); });
return *_ptr;
}
static std::unique_ptr<T> _ptr;
};
template<typename T>
std::unique_ptr<T> SingletonBaseLazy<T>::_ptr; #include <iostream>
class YourSingleton : public SingletonBaseLazy < YourSingleton >
{
public:
void test()
{
std::cout << "hello word" << std::endl;
}
}; int _tmain(int argc, _TCHAR* argv[])
{
YourSingleton::Instance().test();
YourSingleton::Instance().test();
return ;
}

代码已上传 https://github.com/xuhuajie-NetEase/SingletonMode

由std::once_call 引发的单例模式的再次总结,基于C++11的更多相关文章

  1. 【C++11应用】基于C++11及std::thread实现的线程池

    目录 基于C++11及std::thread实现的线程池 基于C++11及std::thread实现的线程池 线程池源码: #pragma once #include <functional&g ...

  2. std::sort引发的core

    #include <stdio.h> #include <vector> #include <algorithm> #include <new> str ...

  3. std::vector与std::list效能对比(基于c++11)

    测试对象类型不同,数量级不同时,表现具有差异: 测试数据对象为std::function时: test: times(1000)vector push_back time 469 usvector e ...

  4. cereal:C++实现的开源序列化库

    闲来无事发现了一个基于C++实现的序列化工具,相比于其他(比如Boost serialization或Google protobuf,恰巧都用过,以后再介绍),使用简单,感觉不错,下面做个摸索. ce ...

  5. facebook开源项目集合

    Facebook的开源大手笔   1. 开源Facebook平台代码 Facebook在2008年选择将该平台上的重要部分的代码和应用工具开源.Facebook称,平台已经基本发展成熟,此举可以让开发 ...

  6. 【Unity技巧】使用单例模式Singleton

    这几天想把在实习里碰到的一些好的技巧写在这里,也算是对实习的一个总结.好啦,今天要讲的是在Unity里应用一种非常有名的设计模式——单例模式. 开场白 单例模式的简单介绍请看前面的链接,当然网上还有很 ...

  7. 智能指针剖析(上)std::auto_ptr与boost::scoped_ptr

    1. 引入 C++语言中的动态内存分配没有自动回收机制,动态开辟的空间需要用户自己来维护,在出函数作用域或者程序正常退出前必须释放掉. 即程序员每次 new 出来的内存都要手动 delete,否则会造 ...

  8. C++智能指针剖析(上)std::auto_ptr与boost::scoped_ptr

    1. 引入 C++语言中的动态内存分配没有自动回收机制,动态开辟的空间需要用户自己来维护,在出函数作用域或者程序正常退出前必须释放掉. 即程序员每次 new 出来的内存都要手动 delete,否则会造 ...

  9. java单例模式实现

    1.最基本的单例模式 /** * @author LearnAndGet * @time 2018年11月13日 * 最基本的单例模式 */ public class SingletonV1 { pr ...

随机推荐

  1. 【selenium】- selenium简介

    本文由小编根据慕课网视频亲自整理,转载请注明出处和作者. 1. Selenium的来历 2. Selenium家庭成员 Selenium RC: Selenium 1 Selenium Webdriv ...

  2. xcode7中搭建python开发环境

    1. 双击打开Xcode 2. 点击File->New->New Project 3. 在左边的面板选择Other,右边选择External Build Sytem,点击Next 4. 输 ...

  3. Codeforces Round #480 (Div. 2) C - Posterized

    题目地址:http://codeforces.com/contest/980/problem/C 官方题解: 题解:一共256个像素网格,可以把这个256个分组,每个分组大小<=k.给出n个像素 ...

  4. CF 13E Holes

    Holes 题意:现在有一排洞,每个洞有一个弹力,能弹到ai之后的洞,球会弹到这个排的外面,现在有2个操作,0 a b 将第a个洞的弹力设为b, 1 a 将球放入第a个洞,求输出进洞的次数 和 弹出这 ...

  5. lightoj 1061 - N Queen Again(状压dp)

    题目链接:http://www.lightoj.com/volume_showproblem.php?problem=1061 题解:显然能满足情况的8皇后的摆法不多,于是便可以用题目给出的状态来匹配 ...

  6. Net Core DocXCore 实现word模板导出

    实际工作中,往往有这样的需求,需要导出word,还有各种各样的样式,于是有了word模板导出. 实现以下几个需求: 1.表单导出 2.表格导出 3.表单表格混合导出 4.实际用例测试 解决方案: 实现 ...

  7. Array List和Linked List实现分析

    一,前言 ​ 先来一张Collection集合图. ​ 今天分享一些关于Collection集合中的List,讲真的集合这东西在网上真是老生常谈了.说实话连本人都觉得腻了(哈哈),但是话又说回来,整个 ...

  8. 最短路问题---Dijkstra算法学习

    Dijkstra又称单源最短路算法,就从一个节点到其他各点的最短路,解决的是有向图的最短路问题 此算法的特点是:从起始点为中心点向外层层扩展,直到扩展到中终点为止. 该算法的条件是所给图的所有边的权值 ...

  9. 【Bit String Reordering UVALive - 6832 】【模拟】

    题意分析 题目讲的主要是给你一个01串,然后给你要变成的01串格式,问你要转换成这一格式最少需要移动的步数. 题目不难,但当时并没有AC,3个小时的个人赛1道没AC,归根到底是没有逼自己去想,又想的太 ...

  10. springboot 定时器 Schdule

    定时器:定时启动任务,执行代码 1.在启动类中加入注解: 2.创建一个类,并且在这个类上加入注解:@Component 3.定义一个方法,在方法上加入注解:@Scheduled(cron=" ...