[转]C#学习笔记15——C#多线程编程

1、基本概念
进程:当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的。
线程:线程是程序中的一个执行流,每一个线程都有本身的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不一样的线程能够执行一样的函数。
多线程:多线程是指程序中包含多个执行流,即在一个程序中能够同时运行多个不一样的线程来执行不一样的任务,也就是说容许单个程序建立多个并行执行的线程来完成各自的任务。
静态属性:这个类全部对象所公有的属性,无论你建立了多少个这个类的实例,可是类的静态属性在内存中只有一个。php

2、多线程的优劣
优势:能够提升CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU能够运行其它的线程而不是等待,这样就大大提升了程序的效率。
缺点:线程也是程序,因此线程须要占用内存,线程越多占用内存也越多;
多线程须要协调和管理,因此须要CPU时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
线程太多会致使控制太复杂,最终可能形成不少Bug;web

3、控制线程的类和方法
类:usingSystem.Threading; Thread类数据库

Thread类经常使用属性:
Name:线程的名称
CurrentThread:当前线程
Priority:线程优先级。在C#应用程序中,用户能够设定5个不一样的优先级,由高到低分别是 Highest,AboveNormal,Normal,BelowNormal,Lowest,在建立线程时若是不指定优先级,那么系统默认为ThreadPriority.Normal。
ThreadState:当前线程状态。这个属性表明了线程运行时状态,在不一样的状况下有不一样的值,咱们有时候能够经过对该值的判断来设计程序流程。C# ThreadState属性的取值以下:
◆Aborted:线程已中止;
◆AbortRequested:线程的Thread.Abort()方法已被调用,可是线程还未中止;
◆Background:线程在后台执行,与属性Thread.IsBackground有关;
◆Running:线程正在正常运行;
◆Stopped:线程已经被中止;
◆StopRequested:线程正在被要求中止;
◆Suspended:线程已经被挂起(此状态下,能够经过调用Resume()方法从新运行);
◆SuspendRequested:线程正在要求被挂起,可是将来得及响应;
◆Unstarted:未调用Thread.Start()开始线程的运行;
◆WaitSleepJoin:线程由于调用了Wait(),Sleep()或Join()等方法处于封锁状态;编程

上面提到了Background状态表示该线程在后台运行,那么后台运行的线程有什么特别的地方呢?其实后台线程跟前台线程只有一个区别,那就是后台线程不妨碍程序的终止。一旦一个进程全部的前台线程都终止后,CLR(通用语言运行环境)将经过调用任意一个存活中的后台进程的Abort()方法来完全终止进程。数组


Thread类经常使用方法:
Start():开始执行线程
Suspend():挂起线程
Resume():恢复被挂起的线程
Sleep():暂停线程
Interrupt():中断线程
Join():阻塞调用线程,直到某个线程终止时为止;一个阻塞调用,直到线程的确是终止了才返回。
Abort():停止线程缓存


建立一个线程通常包含三个步骤:
一、建立入口函数
二、建立入口委托
三、建立线程安全

 

4、线程同步数据结构

多个线程的同步技术:多线程

lock语句app

Interlocaked类

Monitor类

等待句柄

Mutex类

Semaphore类

Event类

ReaderWriterLockSlim

lock语句、Interlocaked类、Monitor类可用于进程内部的同步。Mutex类、Semaphore类、Event类、ReaderWriterLockSlim类提供了多个进程中的线程同步。

.NET Framework的CLR提供了三种方法来完成对共享资源,诸如全局变量域,特定的代码段,静态的和实例化的方法和域。

  (1) 代码域同步:使用Monitor类能够同步静态/实例化的方法的所有代码或者部分代码段。不支持静态域的同步。在实例化的方法中,this指针用于同步;而在静态的方法中,类用于同步,这在后面会讲到。

  (2) 手工同步:使用不一样的同步类(诸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent,AutoResetEvent 和Interlocked等)建立本身的同步机制。这种同步方式要求你本身手动的为不一样的域和方法同步,这种同步方式也能够用于进程间的同步和对共享资源的等待而形成的死锁解除。

  (3) 上下文同步:使用SynchronizationAttribute为ContextBoundObject对象建立简单的,自动的同步。这种同步方式仅用于实例化的方法和域的同步。全部在同一个上下文域的对象共享同一个锁。
Monitor Class

  在给定的时间和指定的代码段只能被一个线程访问,Monitor 类很是适合于这种状况的线程同步。这个类中的方法都是静态的,因此不须要实例化这个类。下面一些静态的方法提供了一种机制用来同步对象的访问从而避免死锁和维护数据的一致性。

  Monitor.Enter方法:在指定对象上获取排他锁。

  Monitor.TryEnter方法:试图获取指定对象的排他锁。

  Monitor.Exit方法:释放指定对象上的排他锁。

  Monitor.Wait方法:释放对象上的锁并阻塞当前线程,直到它从新获取该锁。

  Monitor.Pulse方法:通知等待队列中的线程锁定对象状态的更改。

  Monitor.PulseAll方法:通知全部的等待线程对象状态的更改。

  经过对指定对象的加锁和解锁能够同步代码段的访问。Monitor.Enter, Monitor.TryEnter 和 Monitor.Exit用来对指定对象的加锁和解锁。一旦获取(调用了Monitor.Enter)指定对象(代码段)的锁,其余的线程都不能获取该锁。举个例子来讲吧,线程X得到了一个对象锁,这个对象锁能够释放的(调用Monitor.Exit(object) or Monitor.Wait)。当这个对象锁被释放后,Monitor.Pulse方法和 Monitor.PulseAll方法通知就绪队列的下一个线程进行和其余全部就绪队列的线程将有机会获取排他锁。线程X释放了锁而线程Y得到了锁,同时调用Monitor.Wait的线程X进入等待队列。当从当前锁定对象的线程(线程Y)受到了Pulse或PulseAll,等待队列的线程就进入就绪队列。线程X从新获得对象锁时,Monitor.Wait才返回。若是拥有锁的线程(线程Y)不调用Pulse或PulseAll,方法可能被不肯定的锁定。Pulse, PulseAll andWait必须是被同步的代码段被调用。对每个同步的对象,你须要有当前拥有锁的线程的指针,就绪队列和等待队列(包含须要被通知锁定对象的状态变化的线程)的指针。

  你也许会问,当两个线程同时调用Monitor.Enter会发生什么事情?不管这两个线程地调用Monitor.Enter是多么地接近,实际上确定有一个在前,一个在后,所以永远只会有一个得到对象锁。既然Monitor.Enter是原子操做,那么CPU是不可能偏好一个线程而不喜欢另一个线程的。为了获取更好的性能,你应该延迟后一个线程的获取锁调用和当即释放前一个线程的对象锁。对于private和internal的对象,加锁是可行的,可是对于external对象有可能致使死锁,由于不相关的代码可能由于不一样的目的而对同一个对象加锁。

  若是你要对一段代码加锁,最好的是在try语句里面加入设置锁的语句,而将Monitor.Exit放在finally语句里面。对于整个代码段的加锁,你可使用MethodImplAttribute(在System.Runtime.CompilerServices命名空间)类在其构造器中设置同步值。这是一种能够替代的方法,当加锁的方法返回时,锁也就被释放了。若是须要要很快释放锁,你可使用Monitor类和C# lock的声明代替上述的方法。

  让咱们来看一段使用Monitor类的代码:
public void some_method()
{

int a=100;

int b=0;

Monitor.Enter(this);

//say we do something here.

int c=a/b;

Monitor.Exit(this);

}

  上面的代码运行会产生问题。当代码运行到int c=a/b; 的时候,会抛出一个异常,Monitor.Exit将不会返回。所以这段程序将挂起,其余的线程也将得不到锁。有两种方法能够解决上面的问题。第一个方法是:将代码放入try…finally内,在finally调用Monitor.Exit,这样的话最后必定会释放锁。第二种方法是:利用C#的lock()方法。调用这个方法和调用Monitoy.Enter的做用效果是同样的。可是这种方法一旦代码执行超出范围,释放锁将不会自动的发生。见下面的代码:
public void some_method()
{

int a=100;

int b=0;

lock(this);

//say we do something here.

int c=a/b;

}

  C# lock申明提供了与Monitoy.Enter和Monitoy.Exit一样的功能,这种方法用在你的代码段不能被其余独立的线程中断的状况。
WaitHandle Class

  WaitHandle类做为基类来使用的,它容许多个等待操做。这个类封装了win32的同步处理方法。WaitHandle对象通知其余的线程它须要对资源排他性的访问,其余的线程必须等待,直到WaitHandle再也不使用资源和等待句柄没有被使用。下面是从它继承来的几个类:

  Mutex 类:同步基元也可用于进程间同步。

  AutoResetEvent:通知一个或多个正在等待的线程已发生事件。没法继承此类。

  ManualResetEvent:当通知一个或多个正在等待的线程事件已发生时出现。没法继承此类。

  这些类定义了一些信号机制使得对资源排他性访问的占有和释放。他们有两种状态:signaled 和 nonsignaled。Signaled状态的等待句柄不属于任何线程,除非是nonsignaled状态。拥有等待句柄的线程再也不使用等待句柄时用set方法,其余的线程能够调用Reset方法来改变状态或者任意一个WaitHandle方法要求拥有等待句柄,这些方法见下面:

  WaitAll:等待指定数组中的全部元素收到信号。

  WaitAny:等待指定数组中的任一元素收到信号。

  WaitOne:当在派生类中重写时,阻塞当前线程,直到当前的 WaitHandle 收到信号。

  这些wait方法阻塞线程直到一个或者更多的同步对象收到信号。

  WaitHandle对象封装等待对共享资源的独占访问权的操做系统特定的对象不管是收管代码仍是非受管代码均可以使用。可是它没有Monitor使用轻便,Monitor是彻底的受管代码并且对操做系统资源的使用很是有效率。


Mutex Class

  Mutex是另一种完成线程间和跨进程同步的方法,它同时也提供进程间的同步。它容许一个线程独占共享资源的同时阻止其余线程和进程的访问。Mutex的名字就很好的说明了它的全部者对资源的排他性的占有。一旦一个线程拥有了Mutex,想获得Mutex的其余线程都将挂起直到占有线程释放它。Mutex.ReleaseMutex方法用于释放Mutex,一个线程能够屡次调用wait方法来请求同一个Mutex,可是在释放Mutex的时候必须调用一样次数的Mutex.ReleaseMutex。若是没有线程占有Mutex,那么Mutex的状态就变为signaled,不然为nosignaled。一旦Mutex的状态变为signaled,等待队列的下一个线程将会获得Mutex。Mutex类对应与win32的CreateMutex,建立Mutex对象的方法很是简单,经常使用的有下面几种方法:

  一个线程能够经过调用WaitHandle.WaitOne或WaitHandle.WaitAny 或 WaitHandle.WaitAll获得Mutex的拥有权。若是Mutex不属于任何线程,上述调用将使得线程拥有Mutex,并且WaitOne会当即返回。可是若是有其余的线程拥有Mutex,WaitOne将陷入无限期的等待直到获取Mutex。你能够在WaitOne方法中指定参数即等待的时间而避免无限期的等待Mutex。调用Close做用于Mutex将释放拥有。一旦Mutex被建立,你能够经过GetHandle方法得到Mutex的句柄而给WaitHandle.WaitAny 或 WaitHandle.WaitAll 方法使用。

  下面是一个示例:
public void some_method()
{

int a=100;

int b=20;

Mutex firstMutex = new Mutex(false);

FirstMutex.WaitOne();

//some kind of processing can be done here.

Int x=a/b;

FirstMutex.Close();

}

  在上面的例子中,线程建立了Mutex,可是开始并无申明拥有它,经过调用WaitOne方法拥有Mutex。

Synchronization Events

  同步时间是一些等待句柄用来通知其余的线程发生了什么事情和资源是可用的。他们有两个状态:signaled and nonsignaled。AutoResetEvent 和 ManualResetEvent就是这种同步事件。


AutoResetEvent Class

  这个类能够通知一个或多个线程发生事件。当一个等待线程获得释放时,它将状态转换为signaled。用set方法使它的实例状态变为signaled。可是一旦等待的线程被通知时间变为signaled,它的转台将自动的变为nonsignaled。若是没有线程侦听事件,转台将保持为signaled。此类不能被继承。


ManualResetEvent Class

  这个类也用来通知一个或多个线程事件发生了。它的状态能够手动的被设置和重置。手动重置时间将保持signaled状态直到ManualResetEvent.Reset设置其状态为nonsignaled,或保持状态为nonsignaled直到ManualResetEvent.Set设置其状态为signaled。这个类不能被继承。


Interlocked Class

  它提供了在线程之间共享的变量访问的同步,它的操做时原子操做,且被线程共享.你能够经过Interlocked.Increment或Interlocked.Decrement来增长或减小共享变量.它的有点在因而原子操做,也就是说这些方法能够代一个整型的参数增量而且返回新的值,全部的操做就是一步.你也可使用它来指定变量的值或者检查两个变量是否相等,若是相等,将用指定的值代替其中一个变量的值.


ReaderWriterLock class

  它定义了一种锁,提供惟一写/多读的机制,使得读写的同步.任意数目的线程均可以读数据,数据锁在有线程更新数据时将是须要的.读的线程能够获取锁,当且仅当这里没有写的线程.当没有读线程和其余的写线程时,写线程能够获得锁.所以,一旦writer-lock被请求,全部的读线程将不能读取数据直到写线程访问完毕.它支持暂停而避免死锁.它也支持嵌套的读/写锁.支持嵌套的读锁的方法是ReaderWriterLock.AcquireReaderLock,若是一个线程有写锁则该线程将暂停;

支持嵌套的写锁的方法是ReaderWriterLock.AcquireWriterLock,若是一个线程有读锁则该线程暂停.若是有读锁将容易却是死锁.安全的办法是使用ReaderWriterLock.UpgradeToWriterLock方法,这将使读者升级到写者.你能够用ReaderWriterLock.DowngradeFromWriterLock方法使写者降级为读者.调用ReaderWriterLock.ReleaseLock将释放锁, ReaderWriterLock.RestoreLock将从新装载锁的状态到调用ReaderWriterLock.ReleaseLock之前。

 

5、线程池和定时器——多线程的自动管理

在多线程的程序中,常常会出现两种状况。一种状况下,应用程序中的线程把大部分的时间花费在等待状态,等待某个事件发生,而后才能给予响应;而另一种状况则是线程日常都处于休眠状态,只是周期性地被唤醒。在.net framework里边,咱们使用ThreadPool来对付第一种状况,使用Timer来对付第二种状况。

  ThreadPool类提供一个由系统维护的线程池——能够看做一个线程的容器,该容器须要Windows 2000以上版本的系统支持,由于其中某些方法调用了只有高版本的Windows才有的API函数。你可使用ThreadPool.QueueUserWorkItem()方法将线程安放在线程池里,该方法的原型以下:

  //将一个线程放进线程池,该线程的Start()方法将调用WaitCallback代理对象表明的函数
  publicstatic bool QueueUserWorkItem(WaitCallback);
  //重载的方法以下,参数object将传递给WaitCallback所表明的方法
  publicstatic bool QueueUserWorkItem(WaitCallback, object);


  要注意的是,ThreadPool类也是一个静态类,你不能也没必要要生成它的对象,并且一旦使用该方法在线程池中添加了一个项目,那么该项目将是没有办法取消的。在这里你无需本身创建线程,只需把你要作的工做写成函数,而后做为参数传递给ThreadPool.QueueUserWorkItem()方法就好了,传递的方法就是依靠WaitCallback代理对象,而线程的创建、管理、运行等等工做都是由系统自动完成的,你无须考虑那些复杂的细节问题,线程池的优势也就在这里体现出来了,就好像你是公司老板——只须要安排工做,而没必要亲自动手。
下面的例程演示了ThreadPool的用法。首先程序建立了一个ManualResetEvent对象,该对象就像一个信号灯,能够利用它的信号来通知其它线程,本例中当线程池中全部线程工做都完成之后,ManualResetEvent的对象将被设置为有信号,从而通知主线程继续运行。它有几个重要的方法:Reset(),Set(),WaitOne()。初始化该对象时,用户能够指定其默认的状态(有信号/无信号),在初始化之后,该对象将保持原来的状态不变直到它的Reset()或者Set()方法被调用,Reset()方法将其设置为无信号状态,Set()方法将其设置为有信号状态。WaitOne()方法使当前线程挂起直到ManualResetEvent对象处于有信号状态,此时该线程将被激活。而后,程序将向线程池中添加工做项,这些以函数形式提供的工做项被系统用来初始化自动创建的线程。当全部的线程都运行完了之后,ManualResetEvent.Set()方法被调用,由于调用了ManualResetEvent.WaitOne()方法而处在等待状态的主线程将接收到这个信号,因而它接着往下执行,完成后边的工做。

  usingSystem;
  usingSystem.Collections;
  usingSystem.Threading;

  //这是用来保存信息的数据结构,将做为参数被传递
  public classSomeState
  {
  public intCookie;
  publicSomeState(int iCookie)
  {
    Cookie =iCookie;
  }
  }

  public classAlpha
  {
  publicHashtable HashCount;
  publicManualResetEvent eventX;
  publicstatic int iCount = 0;
  publicstatic int iMaxCount = 0;
  publicAlpha(int MaxCount) 
  {
    HashCount= new Hashtable(MaxCount);
    iMaxCount= MaxCount;
  }

  file://线程池里的线程将调用Beta()方法
  public voidBeta(Object state)
  {
    //输出当前线程的hash编码值和Cookie的值
    Console.WriteLine("{0} {1} :", Thread.CurrentThread.GetHashCode(),
    ((SomeState)state).Cookie);
    Console.WriteLine("HashCount.Count=={0},Thread.CurrentThread.GetHashCode()=={1}", HashCount.Count,Thread.CurrentThread.GetHashCode());
    lock(HashCount) 
    {
    file://若是当前的Hash表中没有当前线程的Hash值,则添加之
    if(!HashCount.ContainsKey(Thread.CurrentThread.GetHashCode()))
      HashCount.Add(Thread.CurrentThread.GetHashCode(), 0);
    HashCount[Thread.CurrentThread.GetHashCode()]= 
((int)HashCount[Thread.CurrentThread.GetHashCode()])+1;
    }

    int iX =2000;
    Thread.Sleep(iX);
    //Interlocked.Increment()操做是一个原子操做,具体请看下面说明
    Interlocked.Increment(refiCount);
    if (iCount== iMaxCount)
    {
    Console.WriteLine();
    Console.WriteLine("SettingeventX ");
    eventX.Set();
    }
  }
  }

  public classSimplePool
  {
  publicstatic int Main(string[] args)
  {
    Console.WriteLine("ThreadPool Sample:");
    bool W2K =false;
    intMaxCount = 10;//容许线程池中运行最多10个线程
    //新建ManualResetEvent对象而且初始化为无信号状态
    ManualResetEventeventX = new ManualResetEvent(false);
    Console.WriteLine("Queuing{0} items to Thread Pool", MaxCount);
    AlphaoAlpha = new Alpha(MaxCount); file://建立工做项
    //注意初始化oAlpha对象的eventX属性
    oAlpha.eventX= eventX;
    Console.WriteLine("Queueto Thread Pool 0");
    try
    {
    file://将工做项装入线程池 
    file://这里要用到Windows 2000以上版本才有的API,因此可能出现NotSupportException异常
    ThreadPool.QueueUserWorkItem(newWaitCallback(oAlpha.Beta),
    newSomeState(0));
    W2K =true;
    }
    catch(NotSupportedException)
    {
    Console.WriteLine("TheseAPI's may fail when called on a non-Windows 2000 system.");
    W2K =false;
    }
    if (W2K)//若是当前系统支持ThreadPool的方法.
    {
    for (intiItem=1;iItem < MaxCount;iItem++)
    {
      //插入队列元素
      Console.WriteLine("Queueto Thread Pool {0}", iItem);
      ThreadPool.QueueUserWorkItem(newWaitCallback(oAlpha.Beta),new SomeState(iItem));
    }
    Console.WriteLine("Waitingfor Thread Pool to drain");
    file://等待事件的完成,即线程调用ManualResetEvent.Set()方法
    eventX.WaitOne(Timeout.Infinite,true);
    file://WaitOne()方法使调用它的线程等待直到eventX.Set()方法被调用
    Console.WriteLine("ThreadPool has been drained (Event fired)");
    Console.WriteLine();
    Console.WriteLine("Loadacross threads");
    foreach(objecto in oAlpha.HashCount.Keys)
    Console.WriteLine("{0}{1}", o, oAlpha.HashCount[o]);
    }
    Console.ReadLine();
    return 0;

  }
  }

  程序中有些小地方应该引发咱们的注意。SomeState类是一个保存信息的数据结构,在上面的程序中,它做为参数被传递给每个线程,你很容易就能理解这个,由于你须要把一些有用的信息封装起来提供给线程,而这种方式是很是有效的。程序出现的InterLocked类也是专为多线程程序而存在的,它提供了一些有用的原子操做,所谓原子操做就是在多线程程序中,若是这个线程调用这个操做修改一个变量,那么其余线程就不能修改这个变量了,这跟lock关键字在本质上是同样的。

  咱们应该完全地分析上面的程序,把握住线程池的本质,理解它存在的意义是什么,这样咱们才能驾轻就熟地使用它。下面是该程序的输出结果:
  Thread PoolSample:
  Queuing 10items to Thread Pool
  Queue toThread Pool 0
  Queue toThread Pool 1
  ...
  ...
  Queue toThread Pool 9
  Waiting forThread Pool to drain
  98 0 :
  HashCount.Count==0,Thread.CurrentThread.GetHashCode()==98
  100 1 :
  HashCount.Count==1,Thread.CurrentThread.GetHashCode()==100
  98 2 :
  ...
  ...
  SettingeventX
  Thread Poolhas been drained (Event fired) 
  Load acrossthreads
  101 2
  100 3
  98 4
  102 1

  与ThreadPool类不一样,Timer类的做用是设置一个定时器,定时执行用户指定的函数,而这个函数的传递是靠另一个代理对象TimerCallback,它必须在建立Timer对象时就指定,而且不能更改。定时器启动后,系统将自动创建一个新的线程,而且在这个线程里执行用户指定的函数。下面的语句初始化了一个Timer对象:
  Timer timer= new Timer(timerDelegate, s,1000, 1000); 

  第一个参数指定了TimerCallback代理对象;第二个参数的意义跟上面提到的WaitCallback代理对象的同样,做为一个传递数据的对象传递给要调用的方法;第三个参数是延迟时间——计时开始的时刻距如今的时间,单位是毫秒;第四个参数是定时器的时间间隔——计时开始之后,每隔这么长的一段时间,TimerCallback所表明的方法将被调用一次,单位也是毫秒。这句话的意思就是将定时器的延迟时间和时间间隔都设为1秒钟。

  定时器的设置是能够改变的,只要调用Timer.Change()方法,这是一个参数类型重载的方法,通常使用的原型以下:
   public boolChange(long, long);

  下面这段代码将前边设置的定时器修改了一下:
  timer.Change(10000,2000); 

  很显然,定时器timer的时间间隔被从新设置为2秒,中止计时10秒后生效。

  下面这段程序演示了Timer类的用法。

  usingSystem;
  usingSystem.Threading; 
  classTimerExampleState 
  {
  public intcounter = 0;
  public Timertmr;
  }

  class App 
  {
  publicstatic void Main()
  {
    TimerExampleStates = new TimerExampleState();

    //建立代理对象TimerCallback,该代理将被定时调用
    TimerCallbacktimerDelegate = new TimerCallback(CheckStatus);

    //建立一个时间间隔为1s的定时器
    Timertimer = new Timer(timerDelegate, s,1000, 1000);
    s.tmr =timer;

    //主线程停下来等待Timer对象的终止
    while(s.tmr!= null)
    Thread.Sleep(0);
    Console.WriteLine("Timerexample done.");
    Console.ReadLine();
  }
  file://下面是被定时调用的方法

  static voidCheckStatus(Object state)
  {
    TimerExampleStates =(TimerExampleState)state;
    s.counter++;
    Console.WriteLine("{0}Checking Status {1}.",DateTime.Now.TimeOfDay, s.counter);
    if(s.counter== 5)
    {
    file://使用Change方法改变了时间间隔
    (s.tmr).Change(10000,2000);
    Console.WriteLine("changed...");
    }
    if(s.counter== 10)
    {
    Console.WriteLine("disposingof timer...");
    s.tmr.Dispose();
    s.tmr =null;
    }
  }
  }

  程序首先建立了一个定时器,它将在建立1秒以后开始每隔1秒调用一次CheckStatus()方法,当调用5次之后,在CheckStatus()方法中修改了时间间隔为2秒,而且指定在10秒后从新开始。当计数达到10次,调用Timer.Dispose()方法删除了timer对象,主线程因而跳出循环,终止程序。程序执行的结果以下:



  上面就是对ThreadPool和Timer两个类的简单介绍,充分利用系统提供的功能,能够为咱们省去不少时间和精力——特别是对很容易出错的多线程程序。同时咱们也能够看到.net Framework强大的内置对象,这些将对咱们的编程带来莫大的方便。

  有 时候你会以为上面介绍的方法好像不够用,对,咱们解决了代码和资源的同步问题,解决了多线程自动化管理和定时触发的问题,可是如何控制多个线程相互之间的联系呢?例如我要到餐厅吃饭,在吃饭以前我先得等待厨师把饭菜作好,以后我开始吃饭,吃完我还得付款,付款方式能够是现金,也能够是信用卡,付款以后我才能离开。分析一下这个过程,我吃饭能够看做是主线程,厨师作饭又是一个线程,服务员用信用卡收款和收现金能够看做另外两个线程,你们能够很清楚地看到其中的关系——我吃饭必须等待厨师作饭,而后等待两个收款线程之中任意一个的完成,而后我吃饭这个线程能够执行离开这个步骤,因而我吃饭才算结束了。事实上,现实中有着比这更复杂的联系,咱们怎样才能很好地控制它们而不产生冲突和重复呢?

  这种状况下,咱们须要用到互斥对象,即System.Threading命名空间中的Mutex类。你们必定坐过出租车吧,事实上咱们能够把Mutex看做一个出租车,那么乘客就是线程了,乘客首先得等车,而后上车,最后下车,当一个乘客在车上时,其余乘客就只有等他下车之后才能够上车。而线程与Mutex对象的关系也正是如此,线程使用Mutex.WaitOne()方法等待Mutex对象被释放,若是它等待的Mutex对象被释放了,它就自动拥有这个对象,直到它调用Mutex.ReleaseMutex()方法释放这个对象,而在此期间,其余想要获取这个Mutex对象的线程都只有等待。

  下面这个例子使用了Mutex对象来同步四个线程,主线程等待四个线程的结束,而这四个线程的运行又是与两个Mutex对象相关联的。其中还用到AutoResetEvent类的对象,如同上面提到的ManualResetEvent对象同样,你们能够把它简单地理解为一个信号灯,使用AutoResetEvent.Set()方法能够设置它为有信号状态,而使用AutoResetEvent.Reset()方法把它设置为无信号状态。这里用它的有信号状态来表示一个线程的结束。

  // Mutex.cs
  usingSystem;
  usingSystem.Threading;

  public classMutexSample
  {
  static MutexgM1;
  static MutexgM2;
  const intITERS = 100;
  staticAutoResetEvent Event1 = new AutoResetEvent(false);
  staticAutoResetEvent Event2 = new AutoResetEvent(false);
  staticAutoResetEvent Event3 = new AutoResetEvent(false);
  staticAutoResetEvent Event4 = new AutoResetEvent(false);

  publicstatic void Main(String[] args)
  {
    Console.WriteLine("MutexSample ...");
    //建立一个Mutex对象,而且命名为MyMutex
    gM1 = newMutex(true,"MyMutex");
    //建立一个未命名的Mutex 对象.
    gM2 = newMutex(true);
    Console.WriteLine("- Main Owns gM1 and gM2");

    AutoResetEvent[]evs = new AutoResetEvent[4];
    evs[0] =Event1; file://为后面的线程t1,t2,t3,t4定义AutoResetEvent对象
    evs[1] =Event2; 
    evs[2] = Event3;
    evs[3] =Event4; 

    MutexSampletm = new MutexSample( );
    Thread t1= new Thread(new ThreadStart(tm.t1Start));
    Thread t2= new Thread(new ThreadStart(tm.t2Start));
    Thread t3= new Thread(new ThreadStart(tm.t3Start));
    Thread t4= new Thread(new ThreadStart(tm.t4Start));
    t1.Start();// 使用Mutex.WaitAll()方法等待一个Mutex数组中的对象所有被释放
    t2.Start();// 使用Mutex.WaitOne()方法等待gM1的释放
    t3.Start();// 使用Mutex.WaitAny()方法等待一个Mutex数组中任意一个对象被释放
    t4.Start();// 使用Mutex.WaitOne()方法等待gM2的释放


    Thread.Sleep(2000);
    Console.WriteLine("- Main releases gM1");
    gM1.ReleaseMutex(); file://线程t2,t3结束条件知足

    Thread.Sleep(1000);
    Console.WriteLine("- Main releases gM2");
    gM2.ReleaseMutex(); file://线程t1,t4结束条件知足

    //等待全部四个线程结束
    WaitHandle.WaitAll(evs);
    Console.WriteLine("...Mutex Sample");
    Console.ReadLine();
  }

  public voidt1Start( )
  {
    Console.WriteLine("t1Startstarted, Mutex.WaitAll(Mutex[])");
    Mutex[]gMs = new Mutex[2];
    gMs[0] =gM1;//建立一个Mutex数组做为Mutex.WaitAll()方法的参数
    gMs[1] =gM2;
    Mutex.WaitAll(gMs);//等待gM1和gM2都被释放
    Thread.Sleep(2000);
    Console.WriteLine("t1Startfinished, Mutex.WaitAll(Mutex[]) satisfied");
    Event1.Set(); file://线程结束,将Event1设置为有信号状态
  }

  public voidt2Start( )
  {
    Console.WriteLine("t2Startstarted, gM1.WaitOne( )");
    gM1.WaitOne();//等待gM1的释放
    Console.WriteLine("t2Startfinished, gM1.WaitOne( ) satisfied");
    Event2.Set();//线程结束,将Event2设置为有信号状态
  }

  public voidt3Start( )
  {
    Console.WriteLine("t3Startstarted, Mutex.WaitAny(Mutex[])");
    Mutex[]gMs = new Mutex[2];
    gMs[0] =gM1;//建立一个Mutex数组做为Mutex.WaitAny()方法的参数
    gMs[1] =gM2;
    Mutex.WaitAny(gMs);//等待数组中任意一个Mutex对象被释放
    Console.WriteLine("t3Startfinished, Mutex.WaitAny(Mutex[])");
    Event3.Set();//线程结束,将Event3设置为有信号状态
  }

  public voidt4Start( )
  {
    Console.WriteLine("t4Startstarted, gM2.WaitOne( )");
    gM2.WaitOne();//等待gM2被释放
    Console.WriteLine("t4Startfinished, gM2.WaitOne( )");
    Event4.Set();//线程结束,将Event4设置为有信号状态
  }
  }

  下面是该程序的执行结果:
  从执行结果能够很清楚地看到,线程t2,t3的运行是以gM1的释放为条件的,而t4在gM2释放后开始执行,t1则在gM1和gM2都被释放了以后才执行。Main()函数最后,使用WaitHandle等待全部的AutoResetEvent对象的信号,这些对象的信号表明相应线程的结束。

 

6、backgroundWorker多线程组件

在VS2005中添加了BackgroundWorker组件,该组件在多线程编程方面使用起来很是方便。BackgroundWorker类中主要用到的有这列属性、方法和事件:
重要属性:
一、CancellationPending获取一个值,指示应用程序是否已请求取消后台操做。经过在DoWork事件中判断CancellationPending属性能够认定是否须要取消后台操做(也就是结束线程);
二、IsBusy获取一个值,指示 BackgroundWorker 是否正在运行异步操做。程序中使用IsBusy属性用来肯定后台操做是否正在使用中;
三、WorkerReportsProgress获取或设置一个值,该值指示BackgroundWorker可否报告进度更新
四、WorkerSupportsCancellation获取或设置一个值,该值指示 BackgroundWorker 是否支持异步取消。设置WorkerSupportsCancellation为true使得程序能够调用CancelAsync方法提交终止挂起的后台操做的请求;
重要方法:
一、CancelAsync请求取消挂起的后台操做
二、RunWorkerAsync开始执行后台操做
三、ReportProgress引起ProgressChanged事件 
重要事件:
一、DoWork调用 RunWorkerAsync 时发生
二、ProgressChanged调用 ReportProgress 时发生
三、RunWorkerCompleted当后台操做已完成、被取消或引起异常时发生
另外还有三个重要的参数是RunWorkerCompletedEventArgs以及DoWorkEventArgs、ProgressChangedEventArgs。
BackgroundWorker的各属性、方法、事件的调用机制和顺序:
                              
从上图可见在整个生活周期内发生了3次重要的参数传递过程:
参数传递1:这次的参数传递是将RunWorkerAsync(Object)中的Object传递到DoWork事件的DoWorkEventArgs.Argument,因为在这里只有一个参数能够传递,因此在实际应用往封装一个类,将整个实例化的类做为RunWorkerAsync的Object传递到DoWorkEventArgs.Argument;
参数传递2:这次是将程序运行进度传递给ProgressChanged事件,实际使用中每每使用给方法和事件更新进度条或者日志信息;
参数传递3:在DoWork事件结束以前,将后台线程产生的结果数据赋给DoWorkEventArgs.Result一边在RunWorkerCompleted事件中调用RunWorkerCompletedEventArgs.Result属性取得后台线程产生的结果。
另外从上图能够看到DoWork事件是在后台线程中运行的,因此在该事件中不可以操做用户界面的内容,若是须要更新用户界面,可使用ProgressChanged事件及RunWorkCompleted事件来实现。

明白了BagkgroundWorker的事件调用顺序和参数传递机制以后在使用该组件用于多线程编程的时候就能够轻松许多了。

下面看我写的一个demo

//咱们假设获取的记录数固定,咱们为此定义一个常量:
private static int MaxRecords = 100;

private void Form1_Load(object sender,EventArgs e)
{

}

/// <summary>
/// 开始
/// </summary>
private void button1_Click(object sender, EventArgs e)
{
if (this.backgroundWorker1.IsBusy)
{
MessageBox.Show("正在执行");
return;
}
this.textBox1.Clear();
//当Start按钮被点击后,RunWorkerAsync方法被掉调用,咱们定义的常量(MaxRecords )看成参数被掺入。
//随后,将会触发其DoWork事件
this.backgroundWorker1.RunWorkerAsync(MaxRecords);
this.button1.Enabled = false;
this.button2.Enabled = true;
}

private voidbackgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
try
{
//调用RetrieveData方法逐条获取数据
e.Result = this.RetrieveData(this.backgroundWorker1, e);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
throw;
}
}

private int RetrieveData(BackgroundWorkerworker, DoWorkEventArgs e)
{
int maxRecords = (int)e.Argument;
int percent = 0;
for (int i = 1; i <= maxRecords; i++)
{
if (worker.CancellationPending)
{
return i;
}

percent = (int)(((double)i /(double)maxRecords) * 100);
worker.ReportProgress(percent, new KeyValuePair<int, string>(i,Guid.NewGuid().ToString()));
System.Threading.Thread.Sleep(100);
}

return maxRecords;
}

//这些操做须要操做UI上的控件,只能在MainThread中进行。
//若是在RetrieveData方法进行的话,因为该方式是一个异步方法,是会抛出异常的
private void backgroundWorker1_ProgressChanged(object sender,ProgressChangedEventArgs e)
{
KeyValuePair<int, string> record = (KeyValuePair<int,string>)e.UserState;
this.label1.Text = string.Format("There are {0} records retrieved!",record.Key);
this.progressBar1.Value = e.ProgressPercentage;
this.textBox1.AppendText(record.Value+"\r\n");
}

private void button2_Click(object sender,EventArgs e)
{
this.backgroundWorker1.CancelAsync();//请求结束后台操做
}
//若是操做正常地结束,BackgroundWorker的RunWorkerCompleted会被触发
private void backgroundWorker1_RunWorkerCompleted(object sender,RunWorkerCompletedEventArgs e)
{
try
{
this.label1.Text = string.Format("Total records: {0}", e.Result);
this.button1.Enabled = true;
this.button2.Enabled = false;
}
catch (TargetInvocationException ex)
{
MessageBox.Show(ex.InnerException.GetType().ToString());
}
}

 

七、注意事项

一、只要有一个前台线程在运行,应用程序的进程就在运行。若是多个前台线程在运行,而Main方法结束了,应用程序的进程就是激活的。直到全部前台线程完成其任务为止。在默认状况下,用Thread类建立的线程是前台线程;线程池中的线程老是后台线程。

二、只有引用对象才能用于锁。关于lock(this), lock(typeof(ClassName))以及lock("thisLock")的讨论。

MSDN上指出,使用lock时不注意可能致使如下问题:

1). 若是实例能够被公共访问,将出现 lock (this) 问题。

2). 若是 MyType 能够被公共访问,将出现 lock (typeof (MyType)) 问题。

3). 因为进程中使用同一字符串的任何其余代码都将共享同一个锁,因此出现 lock(“myLock”) 问题。

下面来讨论这些问题:

首先看lock(this)问题,若是一个类是公有的时,应该避免在基方法或属性中使用lock(this)语句,由于若是有其余人使用你的组件,它并不了解你的组件内部是否使用了锁,若是使用了而使用者又在类外部对类实例尝试加锁,则可能致使一个死锁,下面是我从Google(原地址:http://www.toolazy.me.uk/template.php?content=lock(this)_causes_deadlocks.xml,原程序中因为将主线程睡眠,实际操做系统会自动切换可用线程,于是未能体现出死锁的效果)找到的修改后的例子:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace Test
{
class InternalClass
{
public void TryLockThis()
{
Thread t = new Thread(ThreadFunction);
t.Start();
}

private void ThreadFunction()
{
Thread.Sleep(3000); // 延迟,等待外部对对象实例加锁
Console.WriteLine("尝试经过lock(this)加锁下面的代码块...");

while (true)
{
lock (this)
{
Console.WriteLine("执行内部锁,1秒后继续...");
Thread.Sleep(1000);
Console.WriteLine("内部锁完成...");
}
}
}
}


class ClassMain
{
private InternalClass theClass = new InternalClass();

public ClassMain()
{
theClass.TryLockThis();
Console.WriteLine("在执行内部锁以前对对象进行加锁...");
lock (theClass) // 若是注释掉这句,ThreadFunction()中的lock将执行成功
{
Console.WriteLine("对象被锁定, 在这里咱们得到了一个死锁...");

while (true) { }
}
}

[STAThread]
static void Main(string[] args)
{
ClassMain cm = new ClassMain();

Console.WriteLine("Press Enter toexit");
Console.ReadLine();
}
}
}

能够看到上述的程序会致使一个死锁,程序的执行结果是这样的:

在执行内部锁以前对对象进行加锁...
对象被锁定, 在这里咱们得到了一个死锁...
尝试经过lock(this)加锁下面的代码块...

(能够这样使用:object ob = new object(); lock(ob) ....)

所以,应尽可能避免甚至拒绝使用lock(this)这样的语句。

一样的,lock(typeof(ClassName))也有相似的问题,因为typeof语句返回的是一个类的类型实例,对于一个类来讲只有一个,若是在类的方法或实例方法中使用了typeof(className)这样的语句,而在类的外部又尝试对类进行加锁,一样可能致使死锁。

关于lock("thisLock")的问题,是由.Net的字符串处理模式致使的。
[MSDN]
公共语言运行库经过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或建立的每一个惟一的字符串的一个引用。所以,具备特定值的字符串的实例在系统中只有一个。
例如,若是将同一字符串分配给几个变量,运行库就会从拘留池中检索对该字符串的相同引用,并将它分配给各个变量。

因为这个缘由,假如你lock的字符串对象在拘留池中(一般是编译时显式声明的用引号引发来的字符串),那么,这个对象也有可能被分配到另一个变量,若是这个变量也使用了lock,那么,两个lock语句表面上lock的不一样对象,但实质上倒是同一个对象,这将形成没必要要的阻塞,甚至可能形成死锁。且不亦发现问题。

从下面的程序中能够看出一个问题:
string a = "String Example";
string b = "String Example";
string c = (new StringBuilder()).Append("String Example").ToString();
Console.WriteLine("a==b? {0}", object.ReferenceEquals(a, b));
Console.WriteLine("a==c? {0}", object.ReferenceEquals(a, c));

上面程序执行的结果是:
a==b? True
a==c? False

从上面能够看出,a和b指向的是同一个引用,而lock正是经过引用来区分并加锁临界代码段的。也就是说,若是在咱们一程序的一个部分中使用了lock("thisLock")进行加锁,而在程序的另外一个位置一样也使用lock("thisLock")进行加锁,则极有可能致使一个死锁,于是是很危险的。实际上,不仅是lock("thisLock")这样的语句,在lock中使用string类型的引用都有可能致使死锁。

上述就是C#中应避免使用lock(this),lock(typeof(className)), lock("thisLock")的缘由。

三、线程的基本操做,例如:暂停、继续、中止等?

我不建议使用Thread类提供的Suspend、Resume以及Abort这三个方法,前两个有问题,好像在VS05已经屏蔽这两个方法;对于Abort来讲,除了资源没有获得及时释放外,有时候会出现异常。如何作呢,经过设置开关变量来完成。

四、如何向线程传递参数或者从中获得其返回值?

我不建议使用静态成员来完成,仅仅为了线程而破坏类的封装有些得不偿失。那如何作呢,经过建立单独的线程类来完成。

五、如何使线程所占用的CPU不要总是百分之百?

形成这个缘由是因为线程中进行不间断的循环操做,从而使CPU彻底被子线程占有。那么处理此类问题,其实很简单,在适当的位置调用Thread.Sleep(20)来释放所占有CPU资源,不要小看这20毫秒的睡眠,它的做用但是巨大的,可使其余线程获得CPU资源,从而使你的CPU使用效率降下来。

六、如何在子线程中控制窗体控件?

为何不能直接在子线程中操纵UI呢。缘由在于子线程和UI线程属于不一样的上下文,换句比较通俗的话说,就比如两我的在不一样的房间里同样,那么要你直接操做另外一个房间里的东西,恐怕不行罢,那么对于子线程来讲也同样,不能直接操做UI线程中的对象。

那么如何在子线程中操纵UI线程中的对象呢,.Net提供了Invoke和BeginInvoke这两种方法。简单地说,就是子线程发消息让UI线程来完成相应的操做。

这两个方法有什么区别,Invoke须要等到所调函数的返回,而BeginInvoke则不须要。

用这两个方法须要注意的,有以下三点:

第一个是因为Invoke和BeginInvoke属于Control类型的成员方法,所以调用的时候,须要获得Control类型的对象才能触发,也就是说你要触发窗体作什么操做或者窗体上某个控件作什么操做,须要把窗体对象或者控件对象传递到线程中。

第二个,对于Invoke和BeginInvoke接受的参数属于一个delegate类型,我在之前的文章中使用的是MethodInvoker,这是.Net自带的一个delegate类型,而并不意味着在使用Invoke或者BeginInvoke的时候只能用它。

第三个,使用Invoke和BeginInvoke有个须要注意的,就是当子线程在Form_Load开启的时候,会遇到异常,这是由于触发Invoke的对象尚未彻底初始化完毕。处理此类问题,在开启线程以前显式的调用“this.Show();”,来使窗体显示在线程开启以前。若是此时只是开启线程来初始化显示数据,那我建议你不要使用子线程,用Splash窗体的效果可能更好。这方面能够参看以下的例子。

http://www.syncfusion.com/FAQ/WindowsForms/FAQ_c95c.aspx#q621q

七、线程同步lock,Monitor,同步事件EventWaitHandler,互斥体Mutex的基本用法,在此基础上,咱们对它们用法进行比较,并给出何时须要锁何时不须要的几点建议。最后,介绍几个FCL中线程安全的类,集合类的锁定方式等,作为对线程同步系列的完善和补充。

1).几种同步方法的区别 lock和Monitor是.NET用一个特殊结构实现的,Monitor对象是彻底托管的、彻底可移植的,而且在操做系统资源要求方面可能更为有效,同步速度较快,但不能跨进程同步。lock(Monitor.Enter和Monitor.Exit方法的封装),主要做用是锁定临界区,使临界区代码只能被得到锁的线程执行。Monitor.Wait和Monitor.Pulse用于线程同步,相似信号操做,我的感受使用比较复杂,容易形成死锁。互斥体Mutex和事件对象EventWaitHandler属于内核对象,利用内核对象进行线程同步,线程必需要在用户模式和内核模式间切换,因此通常效率很低,但利用互斥对象和事件对象这样的内核对象,能够在多个进程中的各个线程间进行同步。 互斥体Mutex相似于一个接力棒,拿到接力棒的线程才能够开始跑,固然接力棒一次只属于一个线程(Thread Affinity),若是这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其余全部须要接力棒运行的线程都知道能等着看热闹。 EventWaitHandle 类容许线程经过发信号互相通讯。一般,一个或多个线程在 EventWaitHandle 上阻止,直到一个未阻止的线程调用 Set 方法,以释放一个或多个被阻止的线程。

 2).何时须要锁定 首先要理解锁定是解决竞争条件的,也就是多个线程同时访问某个资源,形成意想不到的结果。好比,最简单的状况是,一个计数器,两个线程同时加一,后果就是损失了一个计数,但至关频繁的锁定又可能带来性能上的消耗,还有最可怕的状况死锁。那么什么状况下咱们须要使用锁,什么状况下不须要呢? 1)只有共享资源才须要锁定 只有能够被多线程访问的共享资源才须要考虑锁定,好比静态变量,再好比某些缓存中的值,而属于线程内部的变量不须要锁定。 2)多使用lock,少用Mutex 若是你必定要使用锁定,请尽可能不要使用内核模块的锁定机制,好比.NET的Mutex,Semaphore,AutoResetEvent和ManuResetEvent,使用这样的机制涉及到了系统在用户模式和内核模式间的切换,性能差不少,可是他们的优势是能够跨进程同步线程,因此应该清楚的了解到他们的不一样和适用范围。 3)了解你的程序是怎么运行的 实际上在web开发中大多数逻辑都是在单个线程中展开的,一个请求都会在一个单独的线程中处理,其中的大部分变量都是属于这个线程的,根本没有必要考虑锁定,固然对于ASP.NET中的Application对象中的数据,咱们就要考虑加锁了。 4)把锁定交给数据库 数据库除了存储数据以外,还有一个重要的用途就是同步,数据库自己用了一套复杂的机制来保证数据的可靠和一致性,这就为咱们节省了不少的精力。保证了数据源头上的同步,咱们多数的精力就能够集中在缓存等其余一些资源的同步访问上了。一般,只有涉及到多个线程修改数据库中同一条记录时,咱们才考虑加锁。 5)业务逻辑对事务和线程安全的要求 这条是最根本的东西,开发彻底线程安全的程序是件很费时费力的事情,在电子商务等涉及金融系统的案例中,许多逻辑都必须严格的线程安全,因此咱们不得不牺牲一些性能,和不少的开发时间来作这方面的工做。而通常的应用中,许多状况下虽然程序有竞争的危险,咱们仍是能够不使用锁定,好比有的时候计数器少一多一,对结果无伤大雅的状况下,咱们就能够不用去管它。

3).InterLocked类 Interlocked 类提供了同步对多个线程共享的变量的访问的方法。若是该变量位于共享内存中,则不一样进程的线程就可使用该机制。互锁操做是原子的,即整个操做是不能由相同变量上的另外一个互锁操做所中断的单元。这在抢先多线程操做系统中是很重要的,在这样的操做系统中,线程能够在从某个内存地址加载值以后可是在有机会更改和存储该值以前被挂起。 咱们来看一个InterLock.Increment()的例子,该方法以原子的形式递增指定变量并存储结果,示例以下:  Increment()方法累加的示例 classInterLockedTest { public staticInt64 i = 0;public staticvoid Add() { for (int i = 0; i < 100000000; i++) {Interlocked.Increment(ref InterLockedTest.i); //InterLockedTest.i= InterLockedTest.i + 1; } } public static void Main(string[] args) { Thread t1= new Thread(newThreadStart(InterLockedTest.Add)); Thread t2 = new Thread(new ThreadStart(InterLockedTest.Add)); t1.Start(); t2.Start();t1.Join(); t2.Join(); Console.WriteLine(InterLockedTest.i.ToString());Console.Read(); } } 输出结果200000000,若是InterLockedTest.Add()方法中用注释掉的语句代替Interlocked.Increment()方法,结果将不可预知,每次执行结果不一样。InterLockedTest.Add()方法保证了加1操做的原子性,功能上至关于自动给加操做使用了lock锁。同时咱们也注意到InterLockedTest.Add()用时比直接用+号加1要耗时的多,因此说加锁资源损耗仍是很明显的。 另外InterLockedTest类还有几个经常使用方法,具体用法能够参考MSDN上的介绍。

 4).集合类的同步 .NET在一些集合类,好比Queue、ArrayList、HashTable和Stack,已经提供了一个供lock使用的对象SyncRoot。用Reflector查看了SyncRoot属性(Stack.SynchRoot略有不一样)的源码以下:

SyncRoot属性源码

public virtual object SyncRoot

{

 get

{ if (this._syncRoot == null)

{ //若是_syncRoot和null相等,将new object赋值给_syncRoot //Interlocked.CompareExchange方法保证多个线程在使用syncRoot时是线程安全的 Interlocked.CompareExchange(ref this._syncRoot,new object(), null);

}

 return this._syncRoot;

}

 }

这里要特别注意的是MSDN提到:从头至尾对一个集合进行枚举本质上并非一个线程安全的过程。即便一个集合已进行同步,其余线程仍能够修改该集合,这将致使枚举数引起异常。若要在枚举过程当中保证线程安全,能够在整个枚举过程当中锁定集合,或者捕捉因为其余线程进行的更改而引起的异常。应该使用下面的代码:

Queue使用lock示例

 Queue q = new Queue();

 lock (q.SyncRoot)

{

foreach (object item in q)

{ //do something }

}

还有一点须要说明的是,集合类提供了一个是和同步相关的方法Synchronized,该方法返回一个对应的集合类的wrapper类,该类是线程安全的,由于他的大部分方法都用lock关键字进行了同步处理。如HashTable的Synchronized返回一个新的线程安全的HashTable实例,代码以下:

Synchronized的使用和理解 //在多线程环境中只要咱们用下面的方式实例化HashTable就能够了 Hashtable ht = Hashtable.Synchronized(newHashtable());

 //如下代码是.NET Framework Class Library实现,增长对Synchronized的认识 [HostProtection(SecurityAction.LinkDemand,Synchronization=true)]

 public static Hashtable Synchronized(Hashtable table)

 {

if (table == null)

{ throw new ArgumentNullException("table"); }

return new SyncHashtable(table);

 } //SyncHashtable的几个经常使用方法,咱们能够看到内部实现都加了lock关键字保证线程安全public override void Add(object key, object value)

{

lock (this._table.SyncRoot)

{ this._table.Add(key,value); }

 }

public override void Clear()

 {

 lock (this._table.SyncRoot)

 { this._table.Clear(); }

}

public override void Remove(object key)

{

lock (this._table.SyncRoot) { this._table.Remove(key);}

 }

线程同步是一个很是复杂的话题,这里只是根据公司的一个项目把相关的知识整理出来,做为工做的一种总结。这些同步方法的使用场景是怎样的?究竟有哪些细微的差异?还有待于进一步的学习和实践。

相关文章
相关标签/搜索