在一个多线程程序中,如果共享资源同时被多个线程使用,就有可能会造成多线程问题,这主要取决于针对该资源的某项操作是否是线程安全的。例如,.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 }
这样,上面的问题就彻底解决了~