摘自:http://blog.csdn.net/sheismylife/archive/2011/05/18/6429946.aspx
采用静态或者全局变量的实现方案
由于C++不能保证静态或者全局对象的构造函数的调用顺序以及析构顺序。所以如果程序中有多个用此方法实现的Singleton类,它们之间又有某种构造 依赖关系和析构依赖关系,就会造成灾难性的后果。所以,只有当肯定不会有构造和析构依赖关系的情况下,这种实现才是合适的。
> | 优点 | 实现简单,多线程下安全 |
> | 缺点 | 如果有多个Singleton对象的创建顺序有依赖时,千万别用;不是lazy loading,有些浪费。 |
Meyers Singleton来控制构造顺序,但是不能控制析构顺序
Scott Meyer在<<Effective C++>>3rd Item4中提出了一个解决方案,当将non-local static变量移动到静态方法中成为local static变量的时候。C++保证当第一次静态方法被调用的时候,才会创建该静态变量。但是这里有一个疑问,创建顺序能够被控制了,可是析构顺序呢?我 们只知道进程结束的时候,local static 变量会被析构,而且按照创建顺序的相反顺序进行。如果几个Singleton类的析构函数之间也有依赖关系,并且这种依赖顺序关系和LIFO顺序冲突,就 会造成dead-reference问题。
> | 优点 | 实现简单;用的时候才创建,比较节省。 |
> | 缺点 | 多线程下不安全;如果有多个Singleton对象的析构顺序有依赖时,要小心 |
DCLP 98标准下是不可靠的,0x标准下是可靠的
DCLP 就是 Double-checked locking pattern.用于在多线程环境下保证只创建Singleton对象。第一次check不用加锁,但是第二次check和创建对象必须加锁。还要注意编 译器可能会优化代码,导致DCLP模式失效。因此要使用volatile 修饰T* pInstance变量。先看一下 DCLP的实现代码:
> class Singleton { > public : > static Singleton* instance() { > if (pInstance == 0) { > Lock lock; > if (pInstance == 0) { > pInstance = new Singleton ; > } > }
> return pInstance; > } > private : > static Singleton * volatile pInstance; > Singleton(){ > } > }; >
在c++98标准下,这是不可靠的。原因有三点:
一,执行顺序得不到保证。编译器会优化代码,从而改变执行顺序。
pInstance = new Singleton; 这个语句会分成三步完成:1.分配内存,2.在已经分配的内存上调用构造函数创建对象,3.将对象赋值给指针pInstance.但是这个顺序很可能会被 改变为1,3,2。如果A线程在1,3执行完后,B线程执行第一个条件判断if(pInstance ==0),此时锁不能起到保护作用。B线程会认为pInstance已经指向有效对象,可以去使用了。嘿嘿,灾难发生。主要原因是C++98标准中没有包 含多线程,只假定是单线程,编译器的优化行为无视多线程环境,因此产生的优化代码可能会被去掉或者改变顺序。我们没有办法在98标准的采用标准c++语言 来解决这个问题,只能采用平台相关的多线程API和与之兼容的编译器来解决。因此,从本质上来说,基于98标准,此问题无解。
二,volatile对于执行顺序也没有帮助。
三,多处理器的系统,B处理器看到变量值的顺序可能和A处理器写变量值的顺序不一致。
详细解释请参考Scott Meyers and Andrei Alexandrescu的论文:http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
技术总是发展的,2011标准的出台给DCLP带来了曙光,真的还能用么,拭目以待吧.
让我们先下个结论:
> | 缺点 | 98标准下不可用 |
> | 优点 | 让我们接受教育,包括Andrei和Scott meyer都犯过错。 |
Andrei说4.11 scott meyer阐述了0x标准下,由于有了线程概念,内存模型,sequence point被sequenced before 和 happens before取代, 有了atomic等等,DCLP又可以复活了。
http://cppandbeyond.com/2011/04/11/session-announcement-the-c0x-memory-model-and-why-you-care/
具体谈了什么呢?我还没有找到相关的文档。
简单锁
于是又回到老土的锁方案,其实注意一下调用,还是能够提高效率的。
> class Singleton { > public : > static Singleton* instance() { > Lock lock; > if (pInstance == 0) { > pInstance = new Singleton ; > }
> return pInstance; > } > private : > static Singleton * volatile pInstance; > Singleton(){ > } > }; >
客户调用时,在每个线程的开头都获得Singleton* p = Singleton::instance();以后就一直使用这个p变量,应该说还是能有效的降低同步的机会。避免频繁调用Singleton::instance()->就好。
> | 优点 | 实现简单,线程安全 |
> | 缺点 | 客户需要意识到,并且遵守少调用的原则。 |
程序开始之前创建Singleton对象
严格来说,这是个策略。将要初始化的放到程序最开始初始化。在这个策略下,以上几种方案都是可以的,包括DCLP。因为总是在单线程中创建对象
摘自:http://hi.baidu.com/luv_resplendent/blog/item/bdb870c81a78f7127e3e6f12.html
volatile的本意是“易变的”
由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:
static int i=0;
int main(void)
{
...
while (1)
{
if (i) dosomething();
}
}
/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}
程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此
可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被
调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。
一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实
现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。
volatile 的含义
volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常 量传播等优化,进一步可以死代码消除。但有时这些优化不是程序所需要的,这时可以用volatile关键字禁止做这些优化,volatile的字面含义是 易变的,它有下面的作用:
1 不会在两个操作之间把volatile变量缓存在寄存器中。在多任务、中断、甚至setjmp环境下,变量可能被其他的程序改变,编译器 自己无法知道,volatile就是告诉编译器这种情况。
2 不做常量合并、常量传播等优化,所以像下面的代码:
volatile int i = 1;
if (i > 0) ...
if的条件不会当作无条件真。
3 对volatile变量的读写不会被优化掉。如果你对一个变量赋值但后面没用到,编译器常常可以省略那个赋值操作,然而对Memory Mapped IO的处理是不能这样优化的。
前面有人说volatile可以保证对内存操作的原子性,这种说法不大准确,其一,x86需要LOCK前缀才能在SMP下保证原子性,其二,RISC根本不能对内存直接运算,要保证原子性得用别的方法,如atomic_inc。
对于jiffies,它已经声明为volatile变量,我认为直接用jiffies++就可以了,没必要用那种复杂的形式,因为那样也不能保证原子性。
你可能不知道在Pentium及后续CPU中,下面两组指令
inc jiffies
;;
mov jiffies, %eax
inc %eax
mov %eax, jiffies
作用相同,但一条指令反而不如三条指令快。