原创 Java Double-Checked Locking 已死,C++ 呢?收藏

新一篇: unspecified_bool_type 手法 | 旧一篇: C++ Multithreading

已经有众多文章讨论 double-checked locking 模式在 Java 下面无法正常工作,这里先简要的总结一下。

根本原因在于 Java 的 memory model 允许所谓的 out-of-order write ,对于下面的 Java 代码,out-of-order write 可能导致灾难性的结果
public static Singleton getInstance()
{
if (instance == null)
{
synchronized(Singleton.class) { //1
if (instance == null) //2
instance = new Singleton(); //3
}
}
return instance;
}

问题的起因在于语句 //3 ,JIT 所生成的汇编代码所作的事情并不是先生成一个 Singleton 对象,然后将其地址赋予 instance 。相反,它的做法是

1. 先申请一块空内存
2. 将其地址赋予 instance
3. 在 instance 所指的地址之上构建对象

下面的汇编代码提供了证明,说明这不只是一个脑筋急转弯,而是实际发生在 JIT 里面的。代码来自 Peter Haggar 的文章,我只是引用一下。

054D20B0   mov         eax,[049388C8]          ;load instance ref
054D20B5   test          eax,eax                         ;test for null
054D20B7   jne           054D20D7
054D20B9   mov         eax,14C0988h
054D20BE   call          503EF8F0                    ;allocate memory
054D20C3   mov         [049388C8],eax          ;store pointer in
                                                                              ;instance ref. instance 
                                                                              ;non-null and ctor
                                                                              ;has not run
054D20C8   mov         ecx,dword ptr [eax]
054D20CA   mov         dword ptr [ecx],1         ;inline ctor - inUse=true;
054D20D0   mov         dword ptr [ecx+4],5     ;inline ctor - val=5;
054D20D7   mov         ebx,dword ptr ds:[49388C8h]
054D20DD   jmp         054D20B0

其中地址为 054D20BE 的代码正在分配内存,而接下来的一行将其赋予 instance ,这个时候 Singleton 的构造函数根本就还没有被调用。

那么问题在哪里?如果线程调度发生在 instance 已经被赋予一个内存地址,而 Singleton 的构造函数还没有被调用的微妙时刻,那么另一个进入此函数的线程会发觉 instance 已经不为 null ,从而放心大胆的将 instance 返回并使用之。但是这个可怜的线程并不知道此时 instance 还没有被初始化呢!

症结在于:首先,构造一个对象不是原子操作,而是可以被打断的;第二,更重要的,Java 允许在初始化之前就把对象的地址写回,这就是所谓 out-of-order 。

那么,对于 C++ 呢?典型的 C++ double-checked locking 可能是这样的

    static Singleton* getInstDC()
    {
        if(inst_ == 0)
        {
            boost::mutex::scoped_lock l(guard_);
            if(inst_ == 0)
                inst_ = new Singleton();
        }
        return inst_;
    }

正如 Java 的行为取决于 JIT 的处理方式,C++ 程序的行为要由编译器来决定。如果某个编译器的处理与 JIT 类似,那么 C++ 程序员也只好对 double-checked locking 说再见。下面是 VC7.1 在 release 配置下生成的代码:

    static Singleton* getInstDC()
    {
00401110  mov         eax,dword ptr fs:[00000000h]
00401116  push        0FFFFFFFFh
00401118  push        offset __ehhandler$?getInstDC@Singleton@@SAPAV1@XZ (4095F8h)
0040111D  push        eax 
        if(inst_ == 0)
0040111E  mov         eax,dword ptr [Singleton::inst_ (40D000h)]
00401123  mov         dword ptr fs:[0],esp
0040112A  sub         esp,8
0040112D  test        eax,eax
0040112F  jne         Singleton::getInstDC+6Eh (40117Eh)
        {
            boost::mutex::scoped_lock l(guard_);
00401131  mov         ecx,offset Singleton::guard_ (40D004h)
00401136  mov         dword ptr [esp],offset Singleton::guard_ (40D004h)
0040113D  call        boost::mutex::do_lock (401340h)
00401142  mov         byte ptr [esp+4],1
            if(inst_ == 0)
00401147  mov         eax,dword ptr [Singleton::inst_ (40D000h)]
0040114C  test        eax,eax
0040114E  mov         dword ptr [esp+10h],0
00401156  jne         Singleton::getInstDC+57h (401167h)
                inst_ = new Singleton();
00401158  push        1   
0040115A  call        operator new (4011A2h)
0040115F  add         esp,4
00401162  mov         dword ptr [Singleton::inst_ (40D000h)],eax
        }
00401167  mov         ecx,offset Singleton::guard_ (40D004h)
0040116C  mov         dword ptr [esp+10h],0FFFFFFFFh
00401174  call        boost::mutex::do_unlock (401360h)
        return inst_;
00401179  mov         eax,dword ptr [Singleton::inst_ (40D000h)]
    }
0040117E  mov         ecx,dword ptr [esp+8]
00401182  mov         dword ptr fs:[0],ecx
00401189  add         esp,14h
0040118C  ret             

从标记为红色的那一句,我们看到了希望:对 inst_ 的赋值发生在 new 完成之后,这意味着至少在 VC7.1 中,我们尚且可以放心使用 double-checked locking ,尽管它未必具有可移植性。


发表于 @ 2005年09月04日 15:00:00|评论(loading...)|编辑

新一篇: unspecified_bool_type 手法 | 旧一篇: C++ Multithreading

评论

#yjh1982 发表于2005-09-05 16:51:00  IP: 211.100.21.*
java不清楚,C++的我觉得没可能失效
只有一个线程能先通过
boost::mutex::scoped_lock l(guard_);来分配内存
而后面通过
boost::mutex::scoped_lock l(guard_);
的线程会再次做检查:
if(inst_ == 0)
#ralph623 发表于2005-09-06 09:09:00  IP: 211.100.21.*
问题的关键应该不在这里,Java 的 synchronized 也同样是加锁。问题在于有没有 out-of-order write 。Java 是明确允许,而 C/C++ 根本就没有规定,这就是我说不可移植的原因。
#ilovevc 发表于2005-09-08 13:37:00  IP: 211.100.21.*
重点不在于 if(inst_ == 0) , 而在于
inst_ = new instance;
这一句在C++中实际上是3句话:
1. operator new (sizeof(instance));
分配内存,
2. 调用instance 构造函数
3. 给inst_ 指针赋值
如果2,3次序颠倒, 那么就会出现inst_为非0但是仅仅是一块raw memory, 结果当然是undefined.



#ilovevc 发表于2005-09-14 09:32:00  IP: 211.100.21.*
这样啊。 那么为什么ACE还是使用DCL呢?
#ralph623 发表于2005-09-13 23:34:00  IP: 211.100.21.*
对,就是这样。不过前几天一个比较资深的工程师提醒了我:现在的x86 CPU 本身就有乱序执行的,所以即便从汇编语句中看不出端倪,也仍然可能产生问题。如果要真的解决,还要了解得更加详细。但是即便我再去仔细钻研了CPU 的文档,得到的结论仍然可能是片面的。所以我决定放弃这个努力,也难怪现在 C++ 社群里面有那么多人在作 multithread memory model 方面的工作,没有它,程序员根本无从准确预测多线程程序的行为。
#非典型秃子 发表于2005-09-22 19:47:00  IP: 211.100.21.*
我怎么觉得这更象是jit优化过了头呢?一种语言,既然要支持多线程,那么这种差错居然要程序员来承担,未免也太过分了吧?要synchronized何用?
#ralph623 发表于2005-10-04 11:19:00  IP: 211.100.21.*
从实用的角度来说,DCL在C++中的确还是可以用的。基本上,CPU的乱序执行总是有个限度的,它可能把相邻的几条指令改变顺序以适应流水线的需要,但是总不可能把前后几十条指令来个乾坤大挪移(我没时间仔细看资料,应该是这样吧)。问题只在于,这一切都是没有保障的,虽然发生问题的几率很低很低。
#刘未鹏 发表于2005-10-25 19:26:00  IP: 211.100.21.*
CPU执行顺序固然是一个威胁,但另一个威胁来自编译器,C++标准没规定说编译器不准调整指令顺序;-)

scott和alexandrescu在DDJ上的一篇文章详细描述了这个问题。

C++的multithreading memory model准备参考或沿用JAVA里面最近已经尘埃落定下来的模型()。

不过进展实在缓慢;-)

#ralph623 发表于2005-11-13 16:03:00  IP: 218.184.170.54, 211.76.97.*
刚刚在看 Hans Boehm 的大作 Threads Cannot be Implemented as a Library ,算是对这个问题有了一个总结,reorder 来自两个方面:

• Compilers may reorder memory operations if that doesn’t violate intra-thread dependencies. Each pair of actions in the above threads could be reordered, since doing so does not change the meaning of each thread, taken in isolation. And performing loads early may result in a better instruction schedule, potentially resulting in performance improvements. (Cf. [2].)

• The hardware may reorder memory operations based on similar constraints. Nearly all common hardware, e.g. X86 processors, may reorder a store followed by a load[17]. Generally a store results immediately in a write-buffer entry, which is later written to a coherent cache, which would then be visible to other threads.

好了,相信这个够全面了。
#jsjzhou 发表于2008-03-19 11:58:31  IP: 58.48.76.*
一个替代方法:

class SingletonTest
{
private static SingletonTest instance;

private SingletonTest
{
}

private static class Instance//嵌套类只加载一次。
{
static fianl SingletonTest Instance=new SingletonTest();
}

public static SingletonTest()
{
return Instance.instance;
}
}
发表评论  


当前用户设置只有注册用户才能发表评论。如果你没有登录,请点击登录
Csdn Blog version 3.1a
Copyright © ralph623