3.多线程
3.1. 锁和线程安全
锁实现互斥的访问,被用于确保在同一时刻只有一个线程可以进入特殊的代码片段,考虑下面的类:
class ThreadUnsafe
{
static int val1, val2;
static void Go()
{
if (val2 != 0) Console.WriteLine (val1 /val2);
val2= 0;
}
}
这不是线程安全的:如果Go方法被两个线程同时调用,可能会得到在某个线程中除数为零的错误,因为val2可能被一个线程设置为零,而另一个线程刚好执行到if和Console.WriteLine语句。
下面用lock来修正这个问题:
class ThreadSafe {
static object locker= new object();
static int val1,val2;
static void Go() {
lock (locker) {
if (val2 != 0) Console.WriteLine(val1 / val2);
val2 = 0;
}
}
}
在同一时刻只有一个线程可以锁定同步对象(在这里是locker),任何竞争的的其它线程都将被阻止,直到这个锁被释放。如果有大于一个的线程竞争这个锁,那么他们将形成称为“就绪队列”的队列,以先到先得的方式授权锁。互斥锁有时被称之对由锁所保护的内容强迫串行化访问,因为一个线程的访问不能与另一个重叠。
一个等候竞争锁的线程被阻止其状态为WaitSleepJoin状态。
C#的lock 语句实际上是调用Monitor.Enter和Monitor.Exit,中间夹杂try-finally语句的简略版,下面是实际发生在之前例子中的Go方法:
Monitor.Enter (locker);
try {
if (val2 != 0)Console.WriteLine (val1 / val2);
val2 = 0;
}
finally {
Monitor.Exit(locker);
}
在同一个对象上,在调用第一个之前Monitor.Enter而先调用了Monitor.Exit将引发异常。
Monitor 也提供了TryEnter方法来实现一个超时功能——也用毫秒或TimeSpan,如果获得了锁返回true,反之没有获得返回false,因为超时了。TryEnter也可以没有超时参数,“测试”一下锁,如果锁不能被获取的话就立刻超时。
3.1.1. 选择同步对象
任何对所有有关系的线程都可见的对象都可以作为同步对象,但要服从一个硬性规定:它必须是引用类型。也强烈建议同步对象最好私有在类里面(比如一个私有实例字段)防止无意间从外部锁定相同的对象。服从这些规则,同步对象可以兼对象和保护两种作用。比如下面List :
class ThreadSafe {
List <string> list = new List<string>();
void Test() {
lock (list) {
list.Add ("Item 1");
...
一个专门字段是常用的,因为它可以精确控制锁的范围和粒度。用对象或类本身的类型作为一个同步对象,即:
lock (this) { ... }
是不好的,因为这潜在的可以在公共范围访问这些对象。
锁并没有以任何方式阻止对同步对象本身的访问,换言之,x.ToString()不会由于另一个线程调用lock(x)而被阻止,两者都要调用lock(x) 来完成阻止工作。
3.1.2. 嵌套锁定
线程可以重复锁定相同的对象,可以通过多次lock语句来实现。线程只能在最开始的锁或最外面的锁上被阻止。当最外面的lock语句完成后,对象那一刻被解锁。
static object x = new object();
static void Main() {
lock (x) {
Console.WriteLine("I have the lock");
Nest();
Console.WriteLine("I still have the lock");
}
在这锁被释放
}
static void Nest() {
lock (x) {
...
} 释放了锁?没有完全释放!
}
作为一项基本规则,任何和多线程有关的会进行读和写的字段应当加锁。在下面的例子中Increment和Assign 都不是线程安全的:
class ThreadUnsafe {
static int x;
static void Increment() { x++; }
static void Assign() { x = 123; }
}
下面是Increment 和 Assign 线程安全的版本:
class ThreadUnsafe {
static object locker =new object();
static int x;
static void Increment() {lock (locker) x++; }
static void Assign() {lock (locker) x = 123; }
}
3.1.4. 锁和原子操作
如果有很多变量在一些锁中总是进行读和写的操作,那么你可以称之为原子操作。我们假设x 和 y不停地读和赋值,他们在锁内通过
locker锁定:
lock (locker) { if (x != 0) y /= x; }
你可以认为x 和 y 通过原子的方式访问,因为代码段没有被其它的线程分开或抢占,别的线程改变x和 y是无效的输出,你永远不会得到除数为零的错误,保证了x 和 y总是被相同的排他锁访问。
3.1.5. 性能考量
锁定本身是非常快的,一个锁在没有堵塞的情况下一般只需几十纳秒(十亿分之一秒)。如果发生堵塞,任务切换带来的开销接近于数微秒(百万分之一秒)的范围内,尽管在线程重组实际的安排时间之前它可能花费数毫秒(千分之一秒)。而相反,与此相形见绌的是该使用锁而没使用的结果就是带来数小时的时间,
对于太多的同步对象死锁是非常容易出现的症状,一个好的规则是开始于较少的锁,在一个可信的情况下涉及过多的阻止出现时,增加锁的粒度。
3.1.6. 死锁
死锁是指多个线程共享某些资源时,都占用一部分资源,而且都在等待对方释放另一部分资源,从而导致程序停滞不前的情况。如下面的例子。
private static object o1 = new object();
private static object o2 = new object();
private static void Work1(){
lock(o1){
…
lock(o2){
…
}
}
}
private static void Work2(){
lock(o2){
…
lock(o1){
…
}
}
}
从上面的例子可以看出,当t1、t2线程分解调用Work1与Work2时。t1进入lock(o1)此时t2进入lock(o2),t1请求lock(o2)时会等待t2释放o2,t2请求lock(o1)时会等待t1释放o1,因此t1与t2相互等待,形成死锁。
3.2. 线程同步
lock语句(也称为Monitor.Enter / Monitor.Exit)是线程同步结构的一个例子。当lock对一段代码或资源实施排他访问时, 但有些同步任务是相当笨拙的或难以实现的,比如说需要传输信号给等待的工作线程使其开始任务执行。
Win32 API拥有丰富的同步系统,这在.NET framework以EventWaitHandle,Mutex 和 Semaphore类展露出来。而一些比有些更有用:例如Mutex类,在EventWaitHandle提供唯一的信号功能时,大多会成倍提高lock的效率。
这三个类都依赖于WaitHandle类,尽管从功能上讲, 它们相当的不同。但它们做的事情都有一个共同点,那就是,被“点名”,这允许它们绕过操作系统进程工作,而不是只能在当前进程里绕过线程。
EventWaitHandle有两个子类:AutoResetEvent 和 ManualResetEvent(不涉及到C#中的事件或委托)。这两个类都派生自它们的基类:它们仅有的不同是它们用不同的参数调用基类的构造函数。性能方面,使用Wait Handles系统开销会花费在微秒间,不会在它们使用的上下文中产生什么后果。
AutoResetEvent在WaitHandle中是最有用的的类,它连同lock语句是一个主要的同步结构。
3.2.1. Synchronization
Synchronization属性和ContextBoundObject类,这两个一起使用可以让一个类的实例处于同步环境中,注意不需要写一大堆lock,只需要在类上有Synchronization这个属性和继承ContextBoundObject,这种数据同步方式简单粗暴,简单是因为只需要做一个声明即可,粗暴是因为在使用该类中的所有数据成员和方法时,都会被锁定,要知道,有时候并不是类中所有的成员,所有的情形都需要进行数据同步的,这就可能是一个严重的性能问题了。
namespace ConsoleApplication2
{
class Program
{
[Synchronization(true)]
class Test :ContextBoundObject
{
public int count = 0;
public voidDisplay()
{
count++;
Console.WriteLine("ContextID:{1},统计:{0}", count, Thread.CurrentContext.ContextID);
}
}
static voidMain(string[] args)
{
Test test = newTest();
Thread thread1 =new Thread(new ThreadStart(() =>
{
for (int i =0; i < 50; i++)
{
test.Display();
}
}));
Thread thread2 =new Thread(new ThreadStart(() =>
{
for (int i =0; i < 50; i++)
{
test.Display();
}
}));
thread1.Start();
thread2.Start();
Console.ReadKey();
}
}
}
这样输出的数是连续的。
3.2.2. Lock
lock关键字很简单,简单的解析就是一把锁,当有一个线程持有这把锁时,其他线程不能进入这个“锁”区域,C#中表现这个区域为一个{}块,称作临界区。
class ThreadSafe {
static object locker= new object();
static int val1,val2;
static void Go() {
lock (locker) {
if (val2 != 0) Console.WriteLine(val1 / val2);
val2 = 0;
}
}
}
3.2.3. Mutex
Mutex称作互斥锁,它可以等待一处的代码同步,也可以等待多处代码同步。作用类似于Lock,Mutex使用WaitOne获取互斥锁,当被抢占后时发生阻止。互斥锁在执行了ReleaseMutex之后被释放。Mutex是相当快的,而lock 又要比它快上数百倍,获取Mutex需要花费几微秒,获取lock需花费数十纳秒。
Mutex是线程相关的,即当一个线程调用WaitOne获得Mutex所有权后,只有该线程能够通过ReleaseMutex()方法释放Mutex的所有权,如果一个线程获得了Mutex的所有权,其他线程调用了该Mutex对象的ReleaseMutex()<