写一个Windows上的守护进程(2)单例

上一篇的日志类的实现里有个这:

class Singleton<CLoggerImpl>

看名字便知其意——单例。这是一个单例模板类。

一个进程通常只有一个日志类实例,这很适合使用单例模式。那么如何设计一个好的单例呢?

通常我们在网上看到有这样的实现:

class SingletonAA
{
public:
static SingletonAA& get_instance_ref()
{
static SingletonAA _inst;
return _inst;
} private:
SingletonAA()
{
//...
}
~SingletonAA()
{
//...
}
};

在首次调用函数get_instance_ref时,构造一个静态实例,我以前也一直用的这种方式,后来看到一些讨论单例的文章,才知道这种实现是有问题的:C++11之前的C++标准并没有指明局部静态变量初始化的线程安全性。就是说,这个静态变量可能被两个线程同时初始化或一个线程初始化了一部分,另一个线程又开始从头初始化。

为了保证线程安全,有的同学可能使用这种方式:

class SingletonAA
{
public:
static SingletonAA& get_instance_ref()
{
if (NULL == p_)
{
p_ = new SingletonAA();
}
return *p_;
} private:
SingletonAA()
{
//...
}
~SingletonAA()
{
//...
} private:
static SingletonAA *p_;
}; SingletonAA *SingletonAA::p_ = NULL;

使用指针,在new之前判断一下指针是否为空。然而这还是有问题:两个线程可能都认为指针为空,然后都去new。

于是有了Double-Checked Locking Pattern (DCLP):

static SingletonAA& get_instance_ref()
{
if (NULL == p_) // 1st check
{
scoped_lock lock;
if (NULL == p_) // 2nd check
{
p_ = new SingletonAA();
}
}
return *p_;
}

做两次判断。因为new是在锁内的,所以不用担心多个线程同时new;进到锁内部之后,又做了一次判断,保证没有别的线程在“第一次判断”和“上锁”这两个动作的间隙new。

这“基本”上已经线程安全了。

但是——嗯,就是有“但是”——这个在C++中还是不对,问题出在这一句:

p_ = new SingletonAA();

不要看这只是一句代码,实际上有三个动作:

1. 分配sizeof(SingletonAA)大小的内存

2. 在分配的这块内存中构造一个SingletonAA对象

3. 使p_指向这块内存

C++并没有规定这三个步骤的执行顺序,但是你也可以想到,第一个步骤肯定是首先执行的。“实践”(来自文末DCLP参考文献)中发现,编译器可能会交换第二步和第三步的执行顺序。我们设想一下,第三步在第二步之前执行的情况:

分配内存

指针赋值

构造对象

如果“指针赋值”之后,这个线程的时间片刚好用完了,另一个线程恰巧又走到“1st check”,发现指针不为空,那就直接开始使用这个未经初始化的对象了!

也许你可能会吐槽,为啥编译器要把“指针赋值”放在“构造对象”之前,“too naïve,我的世界你不懂”编译器君如是回道。若要一探究竟,请阅读文末DCLP参考文献。

照这样说,如果我们把

p_ = new SingletonAA();

这句代码和判断条件分离开就行了,那么这样做:

class SingletonAA
{
public:
static SingletonAA& get_instance_ref()
{
if (!init_flag_) // 1st check
{
scoped_lock lock;
if (!init_flag_) // 2nd check
{
p_ = new SingletonAA();
init_flag_ = true;
}
}
return *p_;
} private:
//... private:
static SingletonAA *p_;
static bool init_flag_;
}; SingletonAA *SingletonAA::p_ = NULL;
bool SingletonAA::init_flag_ = false;

bool在vc2008里是一个字节的,读写只需一条汇编指令(其他版本的vc和g++我都没试),是原子操作,不用考虑线程安全性。

貌似这样就可以了,但是——嗯,又有但是——在get_instance_ref函数中,编译器可能会对init_flag_的读取进行优化:编译器发现你并没有在函数内部对init_flag_赋值,所以实际上它可能仅从变量地址中读取一次,然后放在寄存器中,“2nd check”时从寄存器取,而不从变量地址中取,那么你这两次check的结果就永远是一样的了。

当然,我们是有办法解决这个编译器优化的问题的,想必你知道有个关键字volatile,它的作用就是告诉编译器这个变量是随时会变化的,请不要缓存它的值,每次都从地址中取,我们需要将init_flag_声明成volatile的:

static bool volatile init_flag_;

这样初始化:

bool volatile SingletonAA::init_flag_ = false;

看起来这样就好了,但是我并没有在代码里这样做,因为我不确定这样是不是有问题,我没仔细看完我底下放的两个参考链接指向的文章(这里是我的todo)。

最后还是祭出了大杀器call_once。

call_once,顾名思义,就是仅调用一次。这个东西有个参数是函数对象,它的作用就是保证你给他传递的函数对象只被执行一次,若在执行过程中又有线程过来了,则必须等待执行完毕并以其执行结果为自己的结果。

boost中有对应实现boost::call_once,C++11已经将它纳入标准成为了std::call_once。

我的终极解决方案就用它了:

class SingletonAA
{
public:
static inline SingletonAA& get_instance_ref()
{
boost::call_once(once_, init);
return *p_;
} private:
//... static void init()
{
p_ = new SingletonAA();
} private:
static SingletonAA *p_;
static boost::once_flag once_;
}; SingletonAA *SingletonAA::p_ = NULL;
boost::once_flag SingletonAA::once_ = BOOST_ONCE_INIT;

有兴趣的同学可以看看boost::call_once是怎么实现的(这里是另一个todo)。

这里边还有最后一个问题:资源释放。

我们new了一个对象,却没有delete。

一种办法是显式提供一个销毁函数,这样销毁就必须由调用者保证,不太好;另外的办法就是使用智能指针。

由于项目中好多地方都可能会用到单例,所以为了做的通用一点,我就把单例的实现做成了一个基类,子类不必再去写get_instance_ref之类的代码:

Show you my code:

template<typename Type>
class Singleton : public boost::noncopyable
{
public:
static Type& get_instance_ref()
{
boost::call_once(once_, init);
return *(p_.get());
} protected:
Singleton(){}
virtual ~Singleton(){} private:
static void init()
{
p_.reset(new Type());
} private:
typedef boost::shared_ptr<Type> InstancePtr;
static InstancePtr p_; static boost::once_flag once_;
}; template<typename Type>
boost::once_flag Singleton<Type>::once_ = BOOST_ONCE_INIT; template<typename Type>
typename Singleton<Type>::InstancePtr Singleton<Type>::p_;

使用方法请参考源码。

源码:https://git.oschina.net/mkdym/DaemonSvc.git (主)&& https://github.com/mkdym/DaemonSvc.git (提升逼格用的)。

参考链接:

1. http://silviuardelean.ro/2012/06/05/few-singleton-approaches/ 请自备梯子

2. DCLP:http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf (中译版:http://blog.jobbole.com/86392/

2015年10月25日星期日

**********************************************************************

更新记录:

【2015年11月6日 星期五】set boost::once_flag instance init value to BOOST_ONCE_INIT

【2015年11月10日 星期二】找到一篇参考链接2的中文翻译

【2015年11月12日 星期四】对volatile init_flag_方式的错误性存疑(原本我认为一定是有问题的)

**********************************************************************

写一个Windows上的守护进程(2)单例的更多相关文章

  1. 写一个Windows上的守护进程(8)获取进程路径

    写一个Windows上的守护进程(8)获取进程路径 要想守护某个进程,就先得知道这个进程在不在.我们假设要守护的进程只会存在一个实例(这也是绝大部分情形). 我是遍历系统上的所有进程,然后判断他们的路 ...

  2. 写一个Windows上的守护进程(7)捕获异常并生成dump

    写一个Windows上的守护进程(7)捕获异常并生成dump 谁都不能保证自己的代码不出bug.一旦出了bug,最好是崩溃掉,这样很快就能被发现,若是不崩溃,只是业务处理错了,就麻烦了,可能很长时间之 ...

  3. 写一个Windows上的守护进程(6)Windows服务

    写一个Windows上的守护进程(6)Windows服务 守护进程因为要开机启动,还要高权限,所以我就把它做成Windows服务了. 关于Windows服务的官方文档,大家可以看https://msd ...

  4. 写一个Windows上的守护进程(5)文件系统重定向

    写一个Windows上的守护进程(5)文件系统重定向 在Windows上经常操作文件或注册表的同学可能知道,有"文件系统/注册表重定向"这么一回事.大致来说就是32位程序在64位的 ...

  5. 写一个Windows上的守护进程(4)日志其余

    写一个Windows上的守护进程(4)日志其余 这次把和日志相关的其他东西一并说了. 一.vaformat C++日志接口通常有两种形式:流输入形式,printf形式. 我采用printf形式,因为流 ...

  6. 写一个Windows上的守护进程(3)句柄的管理

    写一个Windows上的守护进程(3)句柄的管理 在Windows中编程,跟HANDLE打交道是家常便饭.为了防止忘记CloseHandle,我都是使用do-while-false手法: void f ...

  7. 写一个Windows上的守护进程(1)开篇

    写一个Windows上的守护进程(1)开篇 最近由于工作需要,要写一个守护进程,主要就是要在被守护进程挂了的时候再把它启起来.说起来这个功能是比较简单的,但是我前一阵子写了好多现在回头看起来比较糟糕的 ...

  8. 写一个Windows服务

    做了两个和Windows服务有关的项目了,最开始的时候没做过,不懂,现在明白了许多.需要注意的是,如果不想登录什么的,最后在添加安装程序的那里选择那个字长的右键属性,把启动方式改为local syst ...

  9. 写一个限制上传文件大小和格式的jQuery插件

    在客户端上传文件,通常需要限制文件的尺寸和格式,最常用的做法是使用某款插件,一些成熟的插件的确界面好看,且功能强大,但美中不足的是:有时候会碰到浏览器兼容问题.本篇就来写一个"原生态&quo ...

随机推荐

  1. Win8.1系统下安装nodeJS

    Nodejs简介 Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境.Node.js 使用了一个事件驱动.非阻塞式 I/O 的模型,使其轻量又高效.Node.js ...

  2. codeforces 340C Tourist Problem(公式题)

    转载请注明出处: http://www.cnblogs.com/fraud/          ——by fraud Tourist Problem Iahub is a big fan of tou ...

  3. memcmp()直接比较两个数组的大小

    两个字符数组可以用strcmp()比较大小.两个整数数组也有个函数memcmp()可以比较大小,和strcmp()的返回值一样的. 头文件#include<cstring> / #incl ...

  4. [原]此程序专用来说明C++模板的用法

    #include using namespace std; //此程序专用来说明模版的使用 template void swap1(T& a,T& b){     T temp=a; ...

  5. 【Python学习】由于windows环境问题导致的不能安装某些需要VC编译的插件

    由于windows环境问题导致的不能安装某些需要VC编译的插件 下载地址:http://www.lfd.uci.edu/~gohlke/pythonlibs/ 安装方法: 在CMD中输入 pip in ...

  6. python bottle 框架开发任务管理系统 V_1.0版

    经过1-2个星期的开发,现在开发了个半成品(UI现在比较烂,因为我的前端本来就很差,将就下吧),大概功能如下:用户功能(添加.删除.修改),添加部门功能,任务管理功能(添加.删除.修改,详细).项目管 ...

  7. MySQL--连接属性

    The capability flags are used by the client and server to indicate which features they support and w ...

  8. Can't create/write to file '/tmp/#sql_3105_0.MYI' (Errcode: 13)

    最近的项目中由于临时存储空间太大了.索性把tmp目录删除了.结果访问出现 Can't create/write to file '/tmp/#sql_3105_0.MYI' (Errcode: 13) ...

  9. window7下statsvn统计代码量

    下载statsvn:http://www.statsvn.org/ 将下载后的statsvn.jar放到d:\svn目录下; 打开cmd窗口切换到需要统计代码的项目目录如:d:\project\web ...

  10. 解决 setOnItemClickListener,setOnScrollListener与setOnTouchListener事件冲突问题

    代码案例如下: lvXxsdMore.setOnItemClickListener(xxsdMoreListener); //对listView 注册onclick事件 lvXxsdMore.setO ...