首页 > 代码库 > 多线程问题与double-check

多线程问题与double-check

在一个多线程程序中,如果共享资源同时被多个线程使用,就有可能会造成多线程问题,这主要取决于针对该资源的某项操作是否是线程安全的。例如,.Net中的dictionary就是一个完全线程不安全的数据结构,对于dictionary的插入、删除都有可能带来多线程问题,这主要是由于dictionary的内部实现结构会频繁的由于插入、删除操作而改变长度,这时,如果出现多线程问题,程序最可能抛出数组越界的Exception。特别的,对于WebService来讲,每一个请求都会生成一个thread/instance,因此就要特别注意多线程问题了。

一般地,多线程问题常常发生于对于共享资源的同时使用。例如,对于类的成员变量的使用,对于全局静态变量的使用,而对于函数内部的局部变量而言,一般式不会存在多线程问题的,因为每个线程在调用一个特定的函数时,都会生成一份函数内部成员变量的副本,线程和线程之间是互不相干的。
解决多线程问题,最常见的方式就是加锁,使得某一资源在同一时刻只能被一个线程所用,而其他线程则必须在被加锁的代码外等待,直到锁被解除,例如如下c#代码所示:

1             lock (_lock)
2             {
3                 //do something to the shared resources.
4             }

 

下面说说double-check。
多线程问题也常常和一种lazy-initialize的设计模式联系在一起。在这里就会慢慢引出double-check。lazy-initialize讲的是,对于一些特别复杂的对象,让程序在第一次调用它的时候再对它进行初始化,而且保证仅仅初始化一次。
首先想到的设计是这样的:

 1     class A
 2     {
 3 
 4 
 5         private ComplexClass _result = null;
 6 
 7         public ComplexClass GetResult()
 8         {
 9             if (_result == null)
10             {
11                 _result = new ComplexClass();
12             }
13             return _result;
14         }
15     }

但是这样有一个问题。ComplexClass的构造过程较长的话,当第一个线程还在进行ComplexClass构造的时候,_result可能是null,也可能指向了一个尚未初始化完成的对象。这样,要么两个线程初始化了两次ComplexClass,要么第二个线程会返回一个指向不完整对象的引用。所以,在这里需要用到一个锁,如下所示:

 1         ComplexClass GetResult()
 2         {
 3             lock (_lock)
 4             {
 5                 if (_result == null)
 6                 {
 7                     _result = new ComplexClass();
 8                 }
 9             }
10             return _result;
11         }

这样,虽然多线程的问题解决了,但是每一次需要使用result时都会请求锁,而请求锁对程序的性能是有很大影响的,因此我们在lock的外面再加一层check:

 1         ComplexClass GetResult()
 2         {
 3             if (_result == null)
 4             {
 5                 lock (_lock)
 6                 {
 7                     if (_result == null)
 8                     {
 9                         _result = new ComplexClass();
10                     }
11                 }
12             }
13             return _result;
14         }

这样,对于所有初始化完成后的请求,就都不用请求锁,而是直接返回_result。
但是还是存在一点问题。对于一些编程语言来说,_result = new ComplexClass();这句代码会使得_result指向一个部分初始化的对象。也就是说,当线程A在初始化ComplexClass时,线程B有可能会判断_result已经不是null了,而这时其实初始化尚未完成,这时线程B就直接返回了一个部分初始化的对象,会造成程序的崩溃。那么,这个问题怎么解决呢?一般的解决方法是在程序内部再加一个局部变量(标识变量)做一层缓冲:

 1         ComplexClass GetResult()
 2         {
 3             ComplexClass result;
 4             if (_result == null)
 5             {
 6                 lock (_lock)
 7                 {
 8                     if (_result == null)
 9                     {
10                         result = new ComplexClass();
11                         _result = result;
12                     }
13                 }
14             }
15             return _result;
16         }

这样,上面的问题就彻底解决了~

多线程问题与double-check