一什么是线程沖突
线程沖突其实就是指两个或以上的线程同时对同一个共享资源进行操作而造成的问题
一个比较经典的例子是用一个全局变量做计数器然后开N个线程去完成某个任务每个线程完成一次任务就将计数器加一直到完成次任务如果不考虑线程沖突问题用类似下面的代码去做则很可能会超额完成任务线程越多完成任务次数超出次的可能性就越大
伪代码如下
int count = ;//全局计数器
void ThreadMethod()//运行在每个线程的方法
{
while( true )
{
if ( count >= )//如果达到任务指标
break;//中断线程执行
DoSomething();//完成某个任务
count++;
}
}
//省略线程的创建等代码
具体的为什么会超额完成任务的原因在这里我就不赘述了这个例子在单线程环境中是绝对不会超额完成任务的
当然在这个例子中将count++放到if语句中也许能降低一些事故发生的概率但那不是绝对的换言之这样的程序不能杜绝超额完成任务的可能
其实从线程沖突的定义中我们不难发现要造成线程沖突有两个必要条件多线程和共享资源这两个条件中有一个不成立就不可能发生线程沖突问题
所以在单线程环境中是不存在线程沖突的问题的不过很可惜的是我们的软件早已进化到了多进程多线程的时代单线程的程序几乎是不存在的无论是WinForm还是WebForm程序运行的环境都是多线程的而不论你自己是不是明确的开启了一个线程
既然多线程是不可避免的那么要避免线程沖突就只能从共享资源来开刀了
二线程安全的资源
如果大家经常看MSDN或者VS帮助中的NET类库参考的话就不难发现几乎所有的类型都有这么一句话的描述此类型的任何公共 static(在 Visual Basic中为 Shared) 成员都是线程安全的但不保证所有实例成员都是线程安全的那么线程安全到底是什么意思?
其实线程安全很简单就是指一个函数(方法属性字段或者别的)在同一时间被不同线程使用不会造成任何线程沖突的问题就说这个东西是线程安全的
接下来来谈谈什么样的资源是线程安全的
之所以使用资源这个词是因为线程沖突不仅仅会发生在共享的变量上两个线程同时对同一个文件进行读写两个程序同时用同一个端口与同一个地址进行通信都会造成线程沖突只不过是操作系统和帮我们协调了这些沖突而已
一个线程安全的资源即是指在不同线程中使用不会导致线程沖突问题的资源
一个不能被改变的资源是线程安全的比如说一个常量
const decimal pai = ;
//C++: const double pai = ;
因为pai的值不可能被改变所以在不同的线程中使用也不会造成沖突换言之它在不同的线程中同时被使用和在一个线程中被使用是没有区别的所以这个东西是线程安全的
同样的在NET中一个字符串的实例也是线程安全的因为字符串的实例在NET中也是不可以被改变的一个字符串的实例一旦被创建对其所有的属性方法调用的结果都是唯一确定的永远不会改变的所以NET类库参考中String类型才有此类型是线程安全的与之类似的Type类型Assembly类型都是线程安全的
但string的实例是线程安全的却不代表string的变量是线程安全的换言之假设有一个静态变量
public static string str = ;
str不是线程安全的因为str这个变量的字符串实例可以被任何线程修改
再考虑这样的例子
public static readonly SqlConnection connection = new SqlConnection( connectionString );
虽然connection本身虽然是线程安全的但connection的任何成员都不是线程安全的
比如说我在一个线程中对这个connection调用了Open方法然后进行查询操作但在同一时刻另一个线程调用了Close方法这时候就出现错误了
但单纯的使用connection而不使用其任何成员比如说if ( connection != null )这样的代码是不存在线程沖突的
线程安全的资源其实还有很多在此不一一赘述
对于NET Framework的类型的成员来说只读的字段是线程安全的
那么对于属性和方法来说怎么知道是不是线程安全的?
三线程安全的函数
因为属性和方法都是函数组成的所以我们探讨一下什么是线程安全的函数
上面我们说到线程沖突的必要条件是多线程和共享资源那么如果一个函数里面没有使用任何可能共享的资源那么就不可能出现线程沖突也就是线程安全的比如说这样的函数
public static int Add( int a int b )
{
return a + b;
}
这个函数中所使用的所有的资源都是自己的局部变量而函数的局部变量是储存在堆栈上的每个线程都有自己独立的堆栈所以局部变量不可能跨线程共享所以这样的函数显然是线程安全的
但值得注意的是下面的函数不是线程安全的
public static void Swap( ref int a ref int b )
//C++: void Swap( in& a int& b )
{
int c = a;
a = b;
b = c;
}
因为ref的存在使得函数的参数是按引用传递进来的换言之a和b看起来是函数的局部变量但实际上却是函数外面的东西如果这两个东西是另一个函数的局部变量倒也没有问题如果这两个东西是全局变量(静态成员)就不能确保没有线程沖突了而在上个例子中a和b在传入函数之时就做了一个拷贝的动作所以传进来的ab到底是全局变量还是静态成员都没有关系了
同样这样的函数也不是线程安全的
public static int Add( INumber a INumber b )
//C++: int Add( INumber* a INumber* b );
{
return aNumber + bNumber;
//C++: return a>Number + b>Number;
}
原因在于a和b虽然是函数的内部变量没错但aNumber和bNumber却不是它们不存在于堆栈上而是在托管堆上可能被其他线程更改
但只使用局部变量的函数在NET类库中是很少的但NET类库中还是有那么多线程安全的函数是为什么呢?
因为即使一个函数使用了共享资源如果其所使用的共享资源都是线程安全的则这个函数也是线程安全的
比如说这样的函数
private const string connectionString = …;
public string GetConnectionString()
{
return connectionString;
}
虽然这个函数使用了一个共享资源connectionString但因为这个资源是线程安全的所以这个函数还是线程安全的
同样的我们可以得出如果一个函数只调用线程安全的函数只使用线程安全的共享资源
那么这个函数也是线程安全的
这里有一个容易被忽略的问题运算符并不是所有的运算符(尤其是重载后的运算符)都是线程安全的
四互斥锁
有时候我们不得不面对线程不安全的问题比如说在一开始提出来的那个例子多线程完成次任务我们怎样才能解决这个问题一个简单的办法就是给共享资源加上互斥锁在C#中这很简单比如一开始的那个例子
public static class Environment
{public static int count = ;//全局计数器
}
//…
void ThreadMethod()//运行在每个线程的方法
{
while( true )
{
lock ( typeof( Environment ) )
{
if ( count >= )//如果达到任务指标
break;//中断线程执行
DoSomething();//完成某个任务
count++;}}}
通过互斥锁使得一个线程在使用count字段的时候其他所有的线程都无法使用而被阻塞等待达到了避免线程沖突的效果
当然这样的锁会使得这个多线程程序退化成同时只有一个线程在跑所以我们可以把count++提前使得lock的范围缩小如这样
void ThreadMethod()//运行在每个线程的方法
{
while( true )
{
lock ( typeof( Environment ) )
{
if ( count++ >= )//如果达到任务指标
break;//中断线程执行
}
DoSomething();//完成某个任务
}}
最后来聊聊SyncRoot的问题
用NET的一定会有很多朋友困惑为什么对一个容器加锁需要这样写
lock( ContainerSyncRoot )
而不是直接lock( Container )
因为锁定一个容器并不能保证不会对这个容器进行修改考虑这样一个容器
public class Collection
{
private ArrayList _list;
public Add( object item )
{
_listAdd( item );
}
public object this[ int index ]
{
get { return _list[index]; }
set { _list[index] = value;}
}}
看起来将其lock起来后就万事大吉了没有人能修改这个容器但实际上这个容器不过是用一个ArrayList实例来实现的如果某段代码绕过这个容器而直接操作_list的话则对这个容器对象lock也不可能保证容器不被修改了