第9章C#多线程技术9.1线程概述Windows是一个多任务的操作系统,如果读者使用的是Windows2000或以上版本,那么可以通过任务管理器查看当前系统运行的进程。什么是进程呢?当一个程序开始运行时,它就是一个进程。进程包括运行中的程序和程序使用的内存和系统资源。而一个进程又是由多个线程组成的,线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说,允许单个程序创建多个并行执行的线程来完成各自的任务。浏览器就是一个很好的多线程例子,在浏览器中可以在下载Java小应用程序或图像的同时滚动页面,在访问新页面时播放动画、声音并打印文件等。多线程程序中,在一个线程必须等待的时候,CPU可以运行其他线程而不是等待,这就大大提高了程序的效率线程不利方面主要有:1)线程也是程序,所以线程需要占用内存,线程越多占用内存也越多。2)多线程需要协调和管理,所以需要占用CPU时间来跟踪线程。3)线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。4)线程太多会导致控制太复杂,最终可能造成很多Bug。1.1C#的优势基于以上认识,我们通过一个比喻来加深理解。假设有一个公司,公司里有很多各司其职的职员,那么这个正常运作的公司就是一个进程,而公司里的职员就是线程。一个公司至少要有一个职员吧,同理,一个进程至少包含一个线程。在公司里,可以让一个职员干所有的事,但是效率显然高不起来;一个程序中也可以只用一个线程去做事。但是,也不是越多越好,公司的职员越多,老板就得发越多的薪水给他们,还得耗费大量精力去管理他们,协调他们之间的矛盾和利益;程序也是如此,线程越多耗费的资源也越多,需要CPU时间去跟踪线程,还得解决诸如死锁、同步等问题。9.2创建并控制一个线程任何程序在执行时,至少有一个主线程,下面这段小程序可以给读者一个直观的印象。usingSystem;usingSystem.Threading;classRunIt{staticvoidMain(string[]args){//给当前线程起名为SystemThreadThread.CurrentThread.Name=SystemThread;Console.WriteLine(Thread.CurrentThread.Name+'Status:+Thread.CurrentThread.ThreadState);}}1.1C#的优势编译执行后程序将产生如下输出:SystemThread'sStatus:Running在这里,我们通过Thread类的静态属性CurrentThread获取了当前执行的线程,对其Name属性赋值“SystemThread”,最后还输出了它的当前状态(ThreadState)。所谓静态属性,就是这个类所有对象公有的属性,不管创建多少个实例,类的静态属性在内存中只有一个。很容易理解CurrentThread为什么是静态的——虽然有多个线程同时存在,但在某一时刻,CPU只能执行其中一个。就像上面程序演示的,我们通过Thread类来创建和控制线程。注意,在程序的头部,我们使用了如下命名空间:usingSystem;usingSystem.Threading;1.1C#的优势在.netframeworkclasslibrary中,所有与多线程机制应用相关的类都放在System.Threading命名空间中。其中Thread类用于创建线程,ThreadPool类用于管理线程池等,此外还提供了解决线程执行中安排、死锁、线程间通信等实际问题的机制。如果想在应用程序中使用多线程,就必须包含这个类。Thread类有几个至关重要的方法,描述如下:(1)Start()——启动线程。(2)Sleep(int)——静态方法,暂停当前线程指定的毫秒数。(3)Abort()——通常使用该方法来终止一个线程。(4)Suspend()——该方法并不终止未完成的线程,而只是挂起线程,以后还可恢复。(5)Resume()——恢复被Suspend()方法挂起的线程的执行。9.2.1线程的创建使用Thread类创建线程时,只需提供线程入口。线程入口告诉程序让这个线程做什么,在C#中,线程入口是通过ThreadStart代理(delegate)提供的,可以把ThreadStart理解为一个函数指针,指向线程要执行的函数,当调用Thread.Start()方法后,线程就开始执行ThreadStart所代表(或说指向)的函数。打开VisualStudio2005,新建一个控制台应用程序,下面的代码是完全控制一个线程的例子。1.1C#的优势usingSystem;usingSystem.Threading;publicclassAlpha{publicvoidBeta(){while(true){Console.WriteLine(Alpha.Betaisrunninginitsownthread.);}}};1.1C#的优势publicclassSimple{publicstaticintMain(){Console.WriteLine(ThreadStart/Stop/JoinSample);AlphaoAlpha=newAlpha();//这里创建一个线程,使之执行Alpha类的Beta()方法ThreadoThread=newThread(newThreadStart(oAlpha.Beta));oThread.Start();while(!oThread.IsAlive);Thread.Sleep(1);oThread.Abort();oThread.Join();Console.WriteLine();Console.WriteLine(Alpha.Betahasfinished);1.1C#的优势try{Console.WriteLine(TrytorestarttheAlpha.Betathread);oThread.Start();}catch(ThreadStateException){Console.Write(ThreadStateExceptiontryingtorestartAlpha.Beta.);Console.WriteLine(Expectedsinceabortedthreadscannotberestarted.);Console.ReadLine();}return0;}}1.1C#的优势这段程序包含两个类Alpha和Simple,在创建线程oThread时我们用指向Alpha.Beta()方法初始化了ThreadStart代理(delegate)对象,当线程oThread调用oThread.Start()方法启动时,实际上程序运行的是Alpha.Beta()方法:AlphaoAlpha=newAlpha();ThreadoThread=newThread(newThreadStart(oAlpha.Beta));oThread.Start();1.1C#的优势然后在Main()函数的while循环中,我们使用静态方法Thread.Sleep()让主线程暂停了1ms,这段时间CPU转向执行线程oThread。然后我们试图用Thread.Abort()方法终止线程oThread,注意后面的oThread.Join(),Thread.Join()方法将使主线程等待,直到oThread线程结束为止。可以给Thread.Join()方法指定一个int型的参数作为等待的最长时间。之后,我们试图用Thread.Start()方法重新启动线程oThread,但是使用Abort()方法的后果是不可恢复地终止线程,所以最后程序会抛出ThreadStateException异常注意:其他线程都依附于Main()函数所在的线程,Main()函数是C#程序的入口,起始线程可以称为主线程,如果所有的前台线程都停止了,那么主线程可以终止,而所有的后台线程都将无条件终止。在微观上,所有的线程是串行执行的,但在宏观上,可以认为它们在并行执行。注意:其他线程都依附于Main()函数所在的线程,Main()函数是C#程序的入口,起始线程可以称为主线程,如果所有的前台线程都停止了,那么主线程可以终止,而所有的后台线程都将无条件终止。在微观上,所有的线程是串行执行的,但在宏观上,可以认为它们在并行执行。9.2.2线程的状态及优先级读者一定注意到了Thread.ThreadState这个属性,它代表了线程运行时的状态,在不同的情况下有不同的值,我们有时通过对该值的判断来设计程序流程。ThreadState在各种情况下的可能取值如下:(1)Aborted——线程已停止;(2)AbortRequested——线程的Thread.Abort()方法已被调用,但是线程还未停止;(3)Background——线程在后台执行,与属性Thread.IsBackground有关;(4)Running——线程正常运行;(5)Stopped——线程已被停止;(6)StopRequested——线程正在被要求停止;(7)Suspended——线程已被挂起(此状态下,可以通过调用Resume()方法重新运行);(8)SuspendRequested——线程正在要求被挂起,但未来得及响应;(9)Unstarted——未调用Thread.Start()开始线程的运行;(10)WaitSleepJoin——线程因为调用了Wait()、Sleep()或Join()等方法而处于封锁状态1.1C#的优势当线程之间争夺CPU时间时,CPU按照线程的优先级给予服务。在C#应用程序中,用户可以设定5个不同的优先级,由高到低分别是Highest、AboveNormal、Normal、BelowNormal、Lowest,在创建线程时如果不指定优先级,系统将默认为ThreadPriority.Normal。给一个线程指定优先级,可以使用如下代码://设定优先级为最低myThread.Priority=ThreadPriority.Lowest;通过设定线程的优先级,可以安排一些相对重要的线程优先执行,如对用户的响应等。现在我们已经初步了解了怎样创建和控制一个线程,下面将深入研究线程实现中比较典型的问题,并探讨解决方法。9.3线程的同步和通信假设有这样一种情况,两个线程同时维护一个队列,如果一个线程向队列添加元素,而另一个线程从队列中取用元素,那么称添加元素的线程为生产者,取用元素的线程为消费者。生产者与消费者问题看起来很简单,却是多线程应用中一个必须解决的问题,它涉及线程之间的同步和通信问题。9.3.1lock关键字前面说过,每个线程都有自己的资源,但代码区是共享的,即每个线程都可以执行相同的函数。但在多线程环境下,可能带来这样的问题:几个线程同时执行一个函数,导致数据混乱,产生不可预料的结果,我们必须避免这种情况发生。C#提供了一个关键字lock,它可以把一段代码定义为互斥段(criticalsection)。互斥段在一个时刻只允许一个线程进入执行,而其他线程必须等待。在C#中,关键字lock定义如下:lock(expression)statement_blockexpression代表希望跟踪的对象,通常是对象引用。一般地,保护一个类的实例,可以使用this;而保护一个静态变量(如互斥代码段在一个静态方法内部),使用类名就可以了。statement_block就是互斥段的代码,这段代码在一个时刻只能被一个线程执行。下面是一个使用lock关键字的典型例子,在注释里说明了lock关键字的用法和用途。【例9.1】lock关键字的使用。usingSystem;usingSystem.Threading;internalclassAccount{intbal