http://blog.cnbang.net/tech/2229/
单例本来是个很简单的模式,实现上应该也是很简单,但C++单例的简单实现会有一些坑,来看看为了避免这些坑怎样一步步演化到boost库的实现方式。
1
2
3
4
5
6
7
8
9
|
class
QMManager
{
public
:
static
QMManager &instance()
{
static
QMManager instance_;
return
instance_;
}
}
|
这是最简单的版本,在单线程下(或者是C++0X下)是没任何问题的,但在多线程下就不行了,因为static QMManager instance_;这句话不是线程安全的。
在局部作用域下的静态变量在编译时,编译器会创建一个附加变量标识静态变量是否被初始化,会被编译器变成像下面这样(伪代码):
01
02
03
04
05
06
07
08
09
10
|
static
QMManager &instance()
{
static
bool
constructed =
false
;
static
uninitialized QMManager instance_;
if
(!constructed) {
constructed =
true
;
new
(&s) QMManager;
//construct it
}
return
instance_;
}
|
这里有竞争条件,两个线程同时调用instance()时,一个线程运行到if语句进入后还没设constructed值,此时切换到另一线程,constructed值还是false,同样进入到if语句里初始化变量,两个线程都执行了这个单例类的初始化,就不再是单例了。
一个解决方法是加锁:
1
2
3
4
5
6
7
|
static
QMManager &instance()
{
Lock();
//锁自己实现
static
QMManager instance_;
UnLock();
return
instance_;
}
|
但这样每次调用instance()都要加锁解锁,代价略大。
那再改变一下,把内部静态实例变成类的静态成员,在外部初始化,也就是在include了文件,main函数执行前就初始化这个实例,就不会有线程重入问题了:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
class
QMManager
{
protected
:
static
QMManager instance_;
QMManager();
~QMManager(){};
public
:
static
QMManager *instance()
{
return
&instance_;
}
void
do_something();
};
QMManager QMManager::instance_;
//外部初始化
|
这被称为饿汉模式,程序一加载就初始化,不管有没有调用到。
看似没问题,但还是有坑,在一个2B情况下会有问题:在这个单例类的构造函数里调用另一个单例类的方法可能会有问题。
看例子:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
//.h
class
QMManager
{
protected
:
static
QMManager instance_;
QMManager();
~QMManager(){};
public
:
static
QMManager *instance()
{
return
&instance_;
}
};
class
QMSqlite
{
protected
:
static
QMSqlite instance_;
QMSqlite();
~QMSqlite(){};
public
:
static
QMSqlite *instance()
{
return
&instance_;
}
void
do_something();
};
QMManager QMManager::instance_;
QMSqlite QMSqlite::instance_;
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
//.cpp
QMManager::QMManager()
{
printf
(
"QMManager constructor\n"
);
QMSqlite::instance()->do_something();
}
QMSqlite::QMSqlite()
{
printf
(
"QMSqlite constructor\n"
);
}
void
QMSqlite::do_something()
{
printf
(
"QMSqlite do_something\n"
);
}
|
这里QMManager的构造函数调用了QMSqlite的instance函数,但此时QMSqlite::instance_可能还没有初始化。
这里的执行流程:程序开始后,在执行main前,执行到QMManager QMManager::instance_;这句代码,初始化QMManager里的instance_静态变量,调用到QMManager的构造函数,在构造函数里调用QMSqlite::instance(),取QMSqlite里的instance_静态变量,但此时QMSqlite::instance_还没初始化,问题就出现了。
那这里会crash吗,测试结果是不会,这应该跟编译器有关,静态数据区空间应该是先被分配了,在调用QMManager构造函数前,QMSqlite成员函数在内存里已经存在了,只是还未调到它的构造函数,所以输出是这样:
QMManager constructor
QMSqlite do_something
QMSqlite constructor
那这个问题怎么解决呢,单例对象作为静态局部变量有线程安全问题,作为类静态全局变量在一开始初始化,有以上2B问题,那结合下上述两种方式,可以解决这两个问题。boost的实现方式是:单例对象作为静态局部变量,但增加一个辅助类让单例对象可以在一开始就初始化。如下:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
//.h
class
QMManager
{
protected
:
struct
object_creator
{
object_creator()
{
QMManager::instance();
}
inline
void
do_nothing()
const
{}
};
static
object_creator create_object_;
QMManager();
~QMManager(){};
public
:
static
QMManager *instance()
{
static
QMManager instance;
return
&instance;
}
};
QMManager::object_creator QMManager::create_object_;
class
QMSqlite
{
protected
:
QMSqlite();
~QMSqlite(){};
struct
object_creator
{
object_creator()
{
QMSqlite::instance();
}
inline
void
do_nothing()
const
{}
};
static
object_creator create_object_;
public
:
static
QMSqlite *instance()
{
static
QMSqlite instance;
return
&instance;
}
void
do_something();
};
QMManager::object_creator QMManager::create_object_;
QMSqlite::object_creator QMSqlite::create_object_;
|
结合方案3的.cpp,这下可以看到正确的输出和调用了:
QMManager constructor
QMSqlite constructor
QMSqlite do_something
来看看这里的执行流程:
初始化QMManager类全局静态变量create_object_
->调用object_creator的构造函数
->调用QMManager::instance()方法初始化单例
->执行QMManager的构造函数
->调用QMSqlite::instance()
->初始化局部静态变量QMSqlite instance
->执行QMSqlite的构造函数,然后返回这个单例。
跟方案三的区别在于QMManager调用QMSqlite单例时,方案3是取到全局静态变量,此时这个变量未初始化,而方案四的单例是静态局部变量,此时调用会初始化。
跟最初方案一的区别是在main函数前就初始化了单例,不会有线程安全问题。
上面为了说明清楚点去除了模版,实际使用是用模版,不用写那么多重复代码,这是boost库的模板实现:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
template
<
typename
T>
class
Singleton
{
struct
object_creator
{
object_creator(){ Singleton
inline
void
do_nothing()
const
{}
};
static
object_creator create_object;
public
:
typedef
T object_type;
static
object_type& instance()
{
static
object_type obj;
//据说这个do_nothing是确保create_object构造函数被调用
//这跟模板的编译有关
create_object.do_nothing();
return
obj;
}
};
template
<
typename
T>
typename
Singleton
class
QMManager
{
protected
:
QMManager(){};
~QMManager(){};
friend
class
Singleton
public
:
void
do_something(){};
};
int
main()
{
Singleton
return
0;
}
|
其实Boost库这样的实现像打了几个补丁,用了一些奇技淫巧,虽然确实绕过了坑实现了需求,但感觉挺不好的。
参考资料:http://blog.csdn.net/fullsail/article/details/8483106
BOOST的Singleton模版详解
首先要说明,这个准确说并不是BOOST的singleton实现,而是BOOST的POOL库的singleton实现。BOOST库中其实有若干个singleton模版,这个只是其中一个。但网上大部分介绍的介绍的BOOST的Singleton实现都是这个,所以大家也就默认了。而且这个的确算是比较特殊和有趣的一个实现。
网上比较有名的文章是这篇《2B程序员,普通程序员和文艺程序员的Singleton实现》 介绍,我虽然对Singleton模版无爱,但自己的项目组中也有人用这个实现,所以还是研究了一下这个实现,特别网上真正解释清楚这个东东的人并不多(包括原文),所以还是研究了一下。
为了介绍清楚这个实现,我们还要先解释清楚为啥2B实现有问题,首先说明,2B实现和BOOST的实现都可以解决多线程调用Singleton导致多次初始化的问题。
但会导致什么问题呢?崩溃?不一定是,(因为静态数据区的空间应该是先分配的),而且结果这个和编译器的实现有关系,
GCC的输出结果如下:
接着我们就来看看BOOST的模版是使用什么技巧,即保证多线程下不重复初始化,又让相互之间的调用更加安全。
首先BOOST的这个实现的Singleton的数据分成两个部分,一个是内部类的object_creator的静态成员creator_object_,一个是instance函数内部的静态变量static T obj;如果外部的有人调用了instance()函数,静态变量obj就会被构造出来,而静态成员creator_object_会在main函数前面构造,他的构造函数内部也调用instance(),这样就会保证静态变量一定会在main函数前面初始化出来。
到此为止,这部分还都能正常理解,但instance()函数中的这句就是有点诡异技巧的了。
create_object_.do_nothing();
其实这句话如果单独分析,并没有明确的作用,因为如果类的静态成员creator_object_的构造就应该让单子对象被初始化。但一旦你注释掉这句话,你会发现create_object_的构造函数都不会被调用。在main函数之前,什么事情都没有发生(VC++2010和GCC都一样),BOOST的代码注释只说是确保create_object_的构造被调用,但也没有明确原因。
我估计这还是和模版的编译有潜在的关系,模版都是Lazy Evaluation。所以如果编译器没有编译过create_object_.do_nothing();编译器就会漏掉create_object_的对象一切实现,也就完全不会编译Singleton_WY
也许是因为我本身对Singleton的模版就不感冒,我对文艺青年的这个Singleton也没有太大胃口,
一方面是技巧性过强,我不才,do_nothing()那句话的问题我研究了半天。
二是由于他将所有的instance初始化放在了main函数前面,好处是避免了多线程多次初始化的麻烦,但也限制了初始化的多样性。一些太强的逻辑关系的情况下这招并不好。
三是这种依靠类static变量的方式,无法按需启动,回收。
四是性能,每次do_nothine也是无谓的消耗呀。
为了一个很简单的风险(多次初始化),引入一个技巧性很强的又有各种限制的东东。是否有点画蛇添足。YY。