对线程同步做一个总结,
一般来说,线程同步比较让人纠结的地方在于它是许多线程共用一段代码的,而且什么时候谁用谁不用,也基本是不可控制不可预料的,那么对于它们可能会同时访问并更改的数据,就需要加锁了。加锁就是将一段代码变为临界区 —— 一段在同一时候只被一个线程进入/执行的代码,加锁的方式一般有两种,Lock关键字
C#提供lock关键字实现临界区,MSDN里给出的用法:
Object thisLock= new Object();
lock (thisLock)
{
// Critical code section
}
此方法的难点是如何选择锁的对象,比如写日志类,如果是静态对象锁自身,那么即使是不冲突的操作,也会互斥,白白浪费了cpu的时间片(像是你所在的a车道可以走了,但非要等到b车道空闲了,你才开始走a车道)
普通的需要实例化的类也要注意,如果是锁this,那么一旦这个对象有多个实例,那么多个实例的操作就变成不互斥了,因为它们锁住的不是同一个thisLock,同时也可以在外部声明一个object,将引用传入临界区,让临界区锁住这个对象,但这种方式比较灵活,但使用起来也需要很小心的维护这个引用。
还有种方法是使用 Monitor
System.Objectobj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
DoSomething();
}
finally
{
System.Threading.Monitor.Exit(obj);
}
Enter和exit 望文生义,就是进入临界区和离开临界区,其实和lock使用起来没什么区别,只是多了个finally,在这里你可以多做一些事情,类似于using的自动dispose和寻常的try finally dispose的区别。不过monitor还有一些其他方法,如Wait和Pulse,可以起到暂停线程和通知拥有锁对象的线程的作用,但我不明白的是,为什么这两个也是静态方法。对于它的运行机制,还不太了解。
还有mutex也可以完成同步,但是用法比较复杂,它的特性是可以在进程中同步,所以线程同步一般不会用它。
System .Object
System .MarshalByRefObject
System.Threading .WaitHandle
System.Threading .Mutex
也可以合理利用volatile 关键字,
(msdn)volatile 关键字指示一个字段可以由多个同时执行的线程修改。 声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。 这样可以确保该字段在任何时间呈现的都是最新的值。
volatile 修饰符通常用于由多个线程访问但不使用 lock 语句对访问进行序 列化的字段。
volatile 关键字可应用于以下类型的字段:
-
引用类型。
-
指针类型(在不安全的上下文中)。请注意,虽然指针本身可以是可变的,但是它指向的对象不能是可变的。换句话说,您无法声明“指向可变对象的指针”。
-
整型,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool。
-
具有整数基类型的枚举类型。
-
已知为引用类型的泛型类型参数。
可变关键字仅可应用于类或结构字段。不能将局部变量声明为 volatile。
线程同步易出错的地方是,全局变量和静态变量,这个必须非常小心。
前面也提到了,线程是共享资源的,所以每一个线程都是访问的同一个全局变量和静态变量(局部变量无此隐患)。
一个很简单的示例,
int 全局变量 i
1 { if(i%2==1)
2 {sysout(i +"是单数")}
3 else { i 是双数}
4 i++ ;
} 这段代码在多个线程里面被并发执行,查看输出,会发现很多错误,因为1执行完毕,进入2或3,但还未执行,这时其他线程可能执行了4,所以i被++了。