在这里我们要注意的是其它线程都是依附于Main()函数所在的线程的,Main()函数是C#程序的入口,起始线程可以称之为主线程,如果所有的前台线程都停止了,那么主线程可以终止,而所有的后台线程都将无条件终止。而所有的线程虽然在微观上是串行执行的,但是在宏观上你完全可以认为它们在并行执行。
牐牰琳咭欢ㄗ⒁獾搅薚hread.ThreadState这个属性,这个属性代表了线程运行时状态,在不同的情况下有不同的值,于是我们有时候可以通过对该值的判断来设计程序流程。ThreadState在各种情况下的可能取值如下:
Aborted:线程已停止 AbortRequested:线程的Thread.Abort()方法已被调用,但是线程还未停止 Background:线程在后台执行,与属性Thread.IsBackground有关 Running:线程正在正常运行 Stopped:线程已经被停止 StopRequested:线程正在被要求停止 Suspended:线程已经被挂起(此状态下,可以通过调用Resume()方法重新运行) SuspendRequested:线程正在要求被挂起,但是未来得及响应 Unstarted:未调用Thread.Start()开始线程的运行 WaitSleepJoin:线程因为调用了Wait(),Sleep()或Join()等方法处于封锁状态牐犐厦嫣岬搅薆ackground状态表示该线程在后台运行,那么后台运行的线程有什么特别的地方呢?其实后台线程跟前台线程只有一个区别,那就是后台线程不妨碍程序的终止。一旦一个进程所有的前台线程都终止后,CLR(通用语言运行环境)将通过调用任意一个存活中的后台进程的Abort()方法来彻底终止进程。
牐牭毕叱讨间争夺CPU时间时,CPU按照是线程的优先级给予服务的。在C#应用程序中,用户可以设定5个不同的优先级,由高到低分别是Highest,AboveNormal,Normal,BelowNormal,Lowest,在创建线程时如果不指定优先级,那么系统默认为ThreadPriority.Normal。给一个线程指定优先级
,我们可以使用如下代码:
牐//设定优先级为最低牐爉yThread.Priority=ThreadPriority.Lowest;
牐犕ü设定线程的优先级,我们可以安排一些相对重要的线程优先执行,例如对用户的响应等等。
牐犗衷谖颐嵌栽跹创建和控制一个线程已经有了一个初步的了解,下面我们将深入研究线程实现中比较典型的的问题,并且探讨其解决方法。
三.线程的同步和通讯生产者和消费者
牐牸偕枵庋一种情况,两个线程同时维护一个队列,如果一个线程对队列中添加元素,而另外一个线程从队列中取用元素,那么我们称添加元素的线程为生产者,称取用元素的线程为消费者。生产者与消费者问题看起来很简单,但是却是多线程应用中一个必须解决的问题,它涉及到线程之间的同步和通讯问题。
牐犌懊嫠倒,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数。但是多线程环境下,可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料的结果,因此我们必须避免这种情况的发生。C#提供了一个关键字lock,它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在C#中,关键字lock定义如下:
牐爈ock(expression) statement_block
expression代表你希望跟踪的对象,通常是对象引用。一般地,如果你想保护一个类的实例,你可以使用this;如果你希望保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。而statement_block就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。
下面是一个使用lock关键字的典型例子,我将在注释里向大家说明lock关键字的用法和用途:
牐//lock.cs牐爑sing System;牐爑sing System.Threading;牐爄nternal class Account牐爗 int balance; Random r = new Random(); internal Account(int initial) { 牐燽alance = initial; } internal int Withdraw(int amount) { 牐爄f (balance 0) 牐爗 file://如果balance小于0则抛出异常 throw new Exception("Negative Balance"); 牐爙 牐//下面的代码保证在当前线程修改balance的值完成之前 牐//不会有其他线程也执行这段代码来修改balance的值 牐//因此,balance的值是不可能小于0的 牐爈ock (this) 牐爗 Console.WriteLine("Current Thread:"+Thread.CurrentThread.Name); file://如果没有lock关键字的保护,那么可能在执行完if的条件判断之后 file://另外一个线程却执行了balance=balance-amount修改了balance的值 file://而这个修改对这个线程是不可见的,所以可能导致这时if的条件已经不成立了 file://但是,这个线程却继续执行balance=balance-amount,所以导致balance可能小于0 if (balance = amount) { 牐燭hread.Sleep(5); 牐燽alance = balance - amount; 牐爎eturn amount; } else { 牐爎eturn 0; // transaction rejected } 牐爙 } internal void DoTransactions() { 牐爁or (int i = 0; i 100; i++) Withdraw(r.Next(-50, 100)); }牐爙牐爄nternal class Test牐爗 static internal Thread[] threads = new Thread[10]; public static void Main() { 牐燗ccount acc = new Account (0); 牐爁or (int i = 0; i 10; i++) 牐爗 Thread t = new Thread(new ThreadStart(acc.DoTransactions)); threads[i] = t; 牐爙 牐爁or (int i = 0; i 10; i++) threads[i].Name=i.ToString(); 牐爁or (int i = 0; i 10; i++) threads[i].Start(); 牐燙onsole.ReadLine(); }牐爙
牐牰多线程公用一个对象时,也会出现和公用代码类似的问题,这种问题就不应该使用lock关键字了,这里需要用到System.Threading中的一个类Monitor,我们可以称之为监视器,Monitor提供了使线程共享资源的方案。
Monitor类可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。对象锁机制保证了在可能引起混乱的情况下一个时刻只有一个线程可以访问这个对象。Monitor必须和一个具体的对象相关联,但是由于它是一个静态的类,所以不能使用它来定义对象,而且它的所有方法都是静态的,不能使用对象来引用。下面代码说明了使用Monitor锁定一个对象的情形:
牐......牐燪ueue oQueue=new Queue();牐......牐燤onitor.Enter(oQueue);牐......//现在oQueue对象只能被当前线程操纵了牐燤onitor.Exit(oQueue);//释放锁
牐犎缟纤示,当一个线程调用Monitor.Enter()方法锁定一个对象时,这个对象就归它所有了,其它线程想要访问这个对象,只有等待它使用Monitor.Exit()方法释放锁。为了保证线程最终都能释放锁,你可以把Monitor.Exit()方法写在try-catch-finally结构中的finally代码块里。对于任何一个被Monitor锁定的对象,内存中都保存着与它相关的一些信息,其一是现在持有锁的线程的引用,其二是一个预备队列,队列中保存了已经准备好获取锁的线程,其三是一个等待队列,队列中保存着当前正在等待这个对象状态改变的队列的引用。当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。
牐犗旅媸且桓稣故救绾问褂胠ock关键字和Monitor类来实现线程的同步和通讯的例子,也是一个典型的生产者与消费者问题。这个例程中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并且显示,我将在注释中介绍该程序的精要所在。用到的系统命名空间如下:
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/bianchengyuyan/)牐爑sing System;牐爑sing System.Threading;
首先,我们定义一个被操作的对象的类Cell,在这个类里,有两个方法:ReadFromCell()和WriteToCell。消费者线程将调用ReadFromCell()读取cellContents的内容并且显示出来,生产者进程将调用WriteToCell()方法向cellContents写入数据。
牐爌ublic class Cell牐爗 int cellContents; // Cell对象里边的内容 bool readerFlag = false; // 状态标志,为true时可以读取,为false则正在写入 public int ReadFromCell( ) { 牐爈ock(this) // Lock关键字保证了什么,请大家看前面对lock的介绍 牐爗 if (!readerFlag)//如果现在不可读取 { 牐爐ry 牐爗 file://等待WriteToCell方法中调用Monitor.Pulse()方法 Monitor.Wait(this); 牐爙 牐燾atch (SynchronizationLockException e) 牐爗 Console.WriteLine(e); 牐爙 牐燾atch (ThreadInterruptedException e) 牐爗 Console.WriteLine(e); 牐爙 } Console.WriteLine("Consume: {0}",cellContents); readerFlag = false; file://重置readerFlag标志,表示消费行为已经完成 Monitor.Pulse(this); file://通知WriteToCell()方法(该方法在另外一个线程中执行,等待中) 牐爙 牐爎eturn cellContents; } public void WriteToCell(int n) { 牐爈ock(this) 牐爗 if (readerFlag) { 牐爐ry 牐爗 Monitor.Wait(this); 牐爙 牐燾atch (SynchronizationLockException e) 牐爗 file://当同步方法(指Monitor类除Enter之外的方法)在非同步的代码区被调用 Console.WriteLine(e); 牐爙 牐燾atch (ThreadInterruptedException e) 牐爗 file://当线程在等待状态的时候中止 Console.WriteLine(e); 牐爙 } cellContents = n; Console.WriteLine("Produce: {0}",cellContents); readerFlag = true; Monitor.Pulse(this); file://通知另外一个线程中正在等待的ReadFromCell()方法 牐爙 }牐爙
牐犗旅娑ㄒ迳产者CellProd和消费者类CellCons,它们都只有一个方法ThreadRun(),以便在Main()函数中提供给线程的ThreadStart代理对象,作为线程的入口。
牐爌ublic class CellProd牐爗 Cell cell; // 被操作的Cell对象 int quantity = 1; // 生产者生产次数,初始化为1 public CellProd(Cell box, int request) { 牐//构造函数 牐燾ell = box; 牐爍uantity = request; } public void ThreadRun( ) { 牐爁or(int looper=1; looper=quantity; looper++) cell.WriteToCell(looper); file://生产者向操作对象写入信息 }牐爙牐爌ublic class CellCons牐爗 Cell cell; int quantity = 1; public CellCons(Cell box, int request) { 牐燾ell = box; 牐爍uantity = request; } public void ThreadRun( ) { 牐爄nt valReturned; 牐爁or(int looper=1; looper=quantity; looper++) valReturned=cell.ReadFromCell( );//消费者从操作对象中读取信息 }牐爙
然后在下面这个类MonitorSample的Main()函数中我们要做的就是创建两个线程分别作为生产者和消费者,使用CellProd.ThreadRun()方法和CellCons.ThreadRun()方法对同一个Cell对象进行操作。
牐爌ublic class MonitorSample牐爗 public static void Main(String[] args) { 牐爄nt result = 0; file://一个标志位,如果是0表示程序没有出错,如果是1表明有错误发生 牐燙ell cell = new Cell( ); 牐//下面使用cell初始化CellProd和CellCons两个类,生产和消费次数均为20次 牐燙ellProd prod = new CellProd(cell, 20); 牐燙ellCons cons = new CellCons(cell, 20); 牐燭hread producer = new Thread(new ThreadStart(prod.ThreadRun)); 牐燭hread consumer = new Thread(new ThreadStart(cons.ThreadRun)); 牐//生产者线程和消费者线程都已经被创建,但是没有开始执行 牐爐ry 牐爗 producer.Start( ); consumer.Start( ); producer.Join( ); consumer.Join( ); Console.ReadLine(); 牐爙 牐燾atch (ThreadStateException e) 牐爗 file://当线程因为所处状态的原因而不能执行被请求的操作 Console.WriteLine(e); result = 1; 牐爙 牐燾atch (ThreadInterruptedException e) 牐爗 file://当线程在等待状态的时候中止 Console.WriteLine(e); result = 1; 牐爙 牐//尽管Main()函数没有返回值,但下面这条语句可以向父进程返回执行结果 牐燛nvironment.ExitCode = result; }牐爙
大家可以看到,在上面的例程中,同步是通过等待Monitor.Pulse()来完成的。首先生产者生产了一个值,而同一时刻消费者处于等待状态,直到收到生产者的脉冲(Pulse)通知它生产已经完成,此后消费者进入消费状态,而生产者开始等待消费者完成操作后将调用Monitor.Pulese()发出的脉冲。它的执行结果很简单:
牐燩roduce: 1牐燙onsume: 1牐燩roduce: 2牐燙onsume: 2牐燩roduce: 3牐燙onsume: 3牐...牐...牐燩roduce: 20牐燙onsume: 20
牐犑率瞪希这个简单的例子已经帮助我们解决了多线程应用程序中可能出现的大问题,只要领悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。
四、线程池和定时器多线程的自动管理
牐犜诙嘞叱痰某绦蛑校经常会出现两种情况。一种情况下,应用程序中的线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应;而另外一种情况则是线程平常都处于休眠状态,只是周期性地被唤醒。在.net framework里边,我们使用ThreadPool来对付第一种情况,使用Timer来对付第二种情况。
牐燭hreadPool类提供一个由系统维护的线程池可以看作一个线程的容器,该容器需要Windows 2000以上版本的系统支持,因为其中某些方法调用了只有高版本的Windows才有的API函数。你可以使用ThreadPool.QueueUserWorkItem()方法将线程安放在线程池里,该方法的原型如下:
牐//将一个线程放进线程池,该线程的Start()方法将调用WaitCallback代理对象代表的函数