写一个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. A Bit Of Knowledge

    iOS推崇使用png格式的图片,说这样不会失帧 imageNamed 和 imageWithContentOfFile的区别 imageNamed会使用系统缓存,对重复加载的图片速度会快一些,效果好. ...

  2. Java IO6 :IO总结

    字节流.字符流继承关系 前几篇文章讲解了字节流.字符流的使用,不过Java提供给用户的流类远不止此,限于篇幅原因,没办法一一讲解,而且也没有必要一一讲解,就像我在写博客的时候多次提到的,有问题的时候学 ...

  3. Qt通过odbc读取excel数据

    传统的读取方式是通过Excel.Application,这种方式不仅操作繁琐,而且速度也不快. 通过odbc读取,可以使用select语句直接读取整个工作表,处理excel数据就跟数据库一样方便. 当 ...

  4. linux命令--virtualenv

    virtualenv可以搭建虚拟且独立的python环境,可以使每个项目环境与其他项目独立开来,保持环境的干净,解决包冲突问题. 一.安装virtualenv virtualenv实际上是一个pyth ...

  5. 如何中途停止RMAN备份任务

    问题背景 如果,你负责的数据库服务器,在RMAN进行全备时,业务又有大量数据要处理,一时间,系统资源直接被耗尽,影响到了业务的正常,你准备怎么处理? 解决办法 [不推荐]当时我们组的另外一个同事在没有 ...

  6. 火狐浏览器,hostadmin hosts文件访问权限不足

    开始->附件->以管理员身份运行. cacls %windir%\system32\drivers\etc\hosts /E /G Users:W

  7. Python爬虫学习:四、headers和data的获取

    之前在学习爬虫时,偶尔会遇到一些问题是有些网站需要登录后才能爬取内容,有的网站会识别是否是由浏览器发出的请求. 一.headers的获取 就以博客园的首页为例:http://www.cnblogs.c ...

  8. python的时间模块

    python有两个重要的时间模块,分别是time和datetime 先看time模块 表示时间的几种方法: 1)时间元组:time.struct_time(tm_year=2016,   tm_mon ...

  9. 实现TCP断点上传,后台C#服务实现接收

    实现TCP断点上传,后台C#服务实现接收 终端实现大文件上传一直都是比较难的技术,其中涉及到后端与前端的交互,稳定性和流量大小,而且实现原理每个人都有自己的想法,后端主流用的比较多的是Http来实现, ...

  10. android style="@[package:]style/style_name" ----------styles.xml

    android.widget ----XML attribute |____style="@[package:]style/style_name"