By zieckey( All right reserved!)
摘要:本文涉及到多线程间数据操作方面的同步,以及volatile关键字的认识误区。从一个实际并发错误例子出发,给出了一步步解决错误的过程,同时详细说明了线程同步机制。
关键字:线程安全;thread-safe;volatile;concurrent;multithread-code;多线程编程
昨天跟同事一起找一个服务器崩溃的bug,牵出一个比较严重的错误认识问题,现在我记录如下,以供大家参考。
我说的尽量简化点,看下面类:
|
这个类是一个网络消息体,在发生消息之前,网络线程会调用 Message::checkEncryption() 来检查是否已经加密,而且这段程序的意图是希望 dosomething() 部分只执行一次。这里几乎没有做任何线程安全方面的工作,于是发生昨晚的杯具。昨晚我们同一个Message会同时发给的客户端,因为是放到网络线程中处理的,所以会出现多个线程同时 Message::checkEncryption() 的情况,那么在某些时候会出现多个线程同时进入到 Message::checkEncryption() 函数体中,从而出现我们不希望看到的问题:dosomething() 这段代码被多个线程多次调用。
我们找到这个问题后,很想当然的认为给这个 Message::checkEncryption() 函数入口处加一把锁就能解决问题,于是有了下面的代码:
|
加如锁之后,原来的问题看似解决了,不然,很快同样的问题继续出现,只是频率要低多了,但是还是偶尔会发生。也就是说,即使加了锁,我们还是发现 Message::checkEncryption() 函数体中的 dosomething(); 这一段代码被多个线程调用过。这是为什么呢?
因为 volatile 修饰的变量仅仅是保证编译器不会优化这个变量,同时也不会放到cpu寄存器中,但似乎并没有说明必须从物理内存中取数据而不从cpu缓存取数据。昨天加锁后问题依旧出现。情况看起来只有这种可能了:
线程A、B同时来到 Message::checkEncryption() 函数体中,其中线程A获得了锁,进入函数体,线程B等待。当线程A退出函数体释放锁,线程B再进入,由于刚刚线程B将锁相关的数据读入cpu缓存中时候顺带也将 m_bIsEncryptedBeforeSend 读入cpu缓存中,这个时候线程B访问m_bIsEncryptedBeforeSend发现是false(old value,并没有因为线程A修改了而得到刷新),于是继续执行,从而出现我们不愿看到的结果,dosomething();被执行两次。(注释:这里线程A、B可能分别在两个不同的cpu核上执行,而每个cpu核可能有自己独立的cpu缓存)。
下面关于 volatile 关键字,我们顺带多说下。
我找了很大一圈关于 volatile 关键字的资料,最终发现网上有两种截然不同的看法:
看法一:用 volatile 变量可以解决多线程中的共享数据问题
看法二:用 volatile 变量不会带来任何好处,根本不能解决多线程共享数据冲突问题,反而会因为不优化而带来性能损降。
昨天出现问题,我发现在多核cpu情况下,看法二是比较符合实际情况的。下面举一些网络上的比较权威的资料:
1. Should volatile Acquire Atomicity and Thread Visibility Semantics?
网址:http:///jtc1/sc22/wg21/docs/papers/2006/n2016.html )
这里说了一句话:According to David Butenhof, "the use of volatile accomplishes nothing but to prevent the compiler from making useful and desirable optimizations, providing no help whatsoever in making code 'thread safe'" (comp.programming.threads posting, July 3, 1997, according to the Google archive). 翻译过来就是:根据David Butenhof所说,使用 volatile 除了阻止编译做一些有益的事情和优化,并不会达到任何目的,对于写多线程代码的线程安全问题不会提供任何帮助。
2. Why the "volatile" type class should not be used
网址:http://kernel.org/doc/Documentation/volatile-considered-harmful.txt 这里提供了一篇文章说明了 "volatile" 变量不应该被使用的原因。
解决上述线程冲突问题,除了加锁外,我们还需要对 m_bIsEncryptedBeforeSend 变量的修改进行原子操作才能保证一个线程(该线程在cpu A核上)改了,另一个线程(该线程可能不在cpu A核而在其他核上)能知道这种更改。具体解决办法请见下面代码:
|
OK.本文的所涉及的code基本上伪代码,不过应该都能很容易的看懂。