写一个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. IT学习方法

    IT 技术的发展日新月异,新技术层出不穷,具有良好的学习能力,能及时获取新知识.随时补充和丰富自己,已成为程序员职业发展的核心竞争力.本文中,作者结合多年的学习经验总结出了提高程序员学习能力的三个要点 ...

  2. (转) 学习C++ -> 向量(vector)

      vector是向量类型,它是一种对象实体,具有值,所以可以看作是变量. 它可以容纳许多其他类型的相同实体,如若干个整数,所以称其为容器.   vector类与一般的Array类的区别在于:   1 ...

  3. [Javascript]史上最短的IE浏览器判断代码

    今天发现个很有趣的js判断全世界最短的代码,想想之前自己写的判断ie浏览器的,这个实在简单多了 var ie = !+"\v1"; 仅仅需要7bytes!参见这篇文章,<32 ...

  4. Scrapy学习系列(一):网页元素查询CSS Selector和XPath Selector

    这篇文章主要介绍创建一个简单的spider,顺便介绍一下对网页元素的选取方式(css selector, xpath selector). 第一步:创建spider工程 打开命令行运行以下命令: sc ...

  5. [C++程序设计]多维数组元素的地址

    设有一个二维数组a,它有3行4列.它的定义为int a[3][4]={{1,3,5,7},{9,11,13,15},{17,18,21,23}};a是一个数组名.a数组包含3行,即3个元 素:a[0] ...

  6. zend framework 初识

    1. 请求顺序 : index.php --> Bootstrap.php --> IndexController.php 2. 验证顺序 : Bootstrap.php function ...

  7. ReferenceError: $ is not defined

    蛋疼的问题,原因是jquery导入顺序不对,任何页面都必须把jquery的导入放在首位,js文件放在其次.

  8. Django学习(五) 定义视图以及页面模板

    请求解析一般都是通过请求的request获取一定参数,然后根据参数做一定业务逻辑判断,这其中可能包括查询数据库,然后将需要返回的数据封装成一个HttpResponse返回. 代码如下: 这是一个简单的 ...

  9. opencv for python

    opencv显示图像: # -*- coding: UTF-8 -*- import numpy as np import cv2 from matplotlib import pyplot as p ...

  10. 在linux中配置tomcat

    Linux下Tomcat的安装配置 一.下载安装对应的jdk,并配置Java环境. 官网下载地址: http://www.oracle.com/technetwork/java/javase/down ...