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

时间:2023-03-08 16:34:54

写一个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_方式的错误性存疑(原本我认为一定是有问题的)

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