现在的位置: 首页 > 综合 > 正文

线程安全——你忽视了么?

2012年03月10日 ⁄ 综合 ⁄ 共 2716字 ⁄ 字号 评论关闭

好久没写blog了,今天还是想写一下关于线程安全的问题。从我以前的blog中可以清楚的知道,我是比较反对使用singleton模式的。这里我只是想举一个非常简单的例子来说明singleton带来的问题很可能比我们想想的要严重的多。

话说我反对使用singleton的主要原因是,singleton的提供者通常无法很好实现线程安全,要么对线程安全的认知,要么干脆认为线程安全什么的无关紧要。

那么一个线程不怎么安全的代码到底会出现写什么问题那?


 

例子1——Random

先来看看这段代码:

 1 using System;
 2 using System.Threading;
 3 
 4 namespace NotThreadSafe
 5 {
 6     class Program
 7     {
 8         static volatile bool s_running = true;
 9         static CountdownEvent s_event = new CountdownEvent(2);
10         static Random s_random = new Random();
11 
12         static void Main(string[] args)
13         {
14             ThreadPool.QueueUserWorkItem(DoRandomHeavily);
15             ThreadPool.QueueUserWorkItem(DoRandomHeavily);
16             Thread.Sleep(1000);
17             s_running = false;
18             s_event.Wait();
19             Console.WriteLine(s_random.Next());
20         }
21 
22         static void DoRandomHeavily(object _)
23         {
24             while (s_running)
25                 s_random.Next();
26             s_event.Signal();
27         }
28     }
29 }

View Code

那么大家来猜猜看,这段代码运行后的输出是什么。

我估计大部分人会说结果在0~int.MaxValue之间的任何一个数,但是实际上,这段代码如果在多核电脑上跑出来的结果(至少是>99.9%的概率)是0。是不是比较意外?


Why?!

来看看为什么结果是如此的诡异,首先,请查阅msdn,msdn上清楚的说明了Random类型不保证实例成员的线程安全,因此,想上面代码中那样使用random是不正确的,会导致一些问题。

有很多人认为random的作用本来就就是随便给个数,线程不安全也无所谓,不还是随便给个数。

不过.net把random设计成一个可以通过seed重复生成某个序列的类,虽然这个序列看起来很random,实际上还是一个算法的,有算法不是问题,问题是这个算法依赖于一些实例字段。通过反编译,可以发现random有三个重要的实力字段:一个int[]用于存储一堆数字;两个int,用于决定取数组中的那两个数来做一系列操作(核心是一个减法)。

显然读取两个int的字段和写入两个int的字段都不是原子的,因此在多线程环境中,很可能会出现读取第一个int字段的指是来自当前cpu本地的缓存值,而在读取第二个int字段之前,当前cpu的缓存刷新了,读取出来的值变成来自其他cpu写入的值。虽然这看起来确实改变了random应有的序列,但是还不至于影响到我们的目的——随机。但是别忘了,这个改变可以导致两个int字段的数值是相同的情况,而且这个概率已经大到在数学上还不能被称为小概率!

一旦两个int字段变成了相同的值,那么噩梦就开始了,从数组的相同位置取出两次数,那么在>99%的概率下是相同的(<1%的是遇到多线程问题,算你狗屎运。。。),两数相减得0,做一系列运算,还是0,再存入数组,一圈转下来,只要中间不发什么多线程问题之类的,数组就全被刷成了0,之后的两个int在这么随便指都无所谓了,因为0减0一定是0。


 

还不过瘾?例子2-Dictionary

来看下代码:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Threading;
 4 
 5 namespace NotThreadSafe
 6 {
 7     class Program
 8     {
 9         static volatile bool s_running = true;
10         static CountdownEvent s_event = new CountdownEvent(2);
11         static Dictionary<int, int> s_dict = new Dictionary<int, int>();
12 
13         static void Main(string[] args)
14         {
15             ThreadPool.QueueUserWorkItem(DoItHeavily);
16             ThreadPool.QueueUserWorkItem(DoItHeavily);
17             Thread.Sleep(1000);
18             s_running = false;
19             s_event.Wait();
20             Console.WriteLine(s_dict.Count);
21         }
22 
23         static void DoItHeavily(object _)
24         {
25             while (s_running)
26             {
27                 s_dict[1] = 1;
28                 s_dict.Remove(1);
29             }
30             s_event.Signal();
31         }
32     }
33 }

View Code

继续问,大家认为会输出什么?

没看懂上面的人估计会说0,看懂了上面的人也许会说程序报错崩溃,不过事实总是让人意外,在多核心机上跑出的结果通常是程序无法退出,并且直接占用两个核的计算资源(即:双核是100%占用,4核是50%占用...)

为什么,简单的说就是:Dictionary的内部数据结构被多线程破坏,导致在Add时直接陷入了死循环。如果想听复杂的解释,不妨自己去抓下dump。


更多的例子我就不举了,回到msdn查一下线程安全,不难发现Framework为我们提供的99%的类型都写着不保证实例成员的线程安全,几乎只有个别类型会写着线程安全,而且这里面的大部分还是那些用于处理线程安全的锁类型,那么singleton的提供者们,请看下代码确实线程安全了么?该加锁的都加了么?如果保证不了线程安全,那么只要是多线程环境,外加使用的足够heavy,1秒内就可以使代码瞬间崩塌。

PS:代码部分以.net为例,但是别认为只有.net有这个问题哦,所有基于线程编程的oop语言都会有这个问题,根据我对部分java的程序员的了解,大部分还是很喜欢拿着spring直接注入一个对象,方式么——singleton,问原因,省内存啊。。。

 

 

 

 

抱歉!评论已关闭.