《C#多线程机制》由会员分享,可在线阅读,更多相关《C#多线程机制(14页珍藏版)》请在金锄头文库上搜索。
1、注 : 本 文 中 出 现 的 代 码 均 在F r a m e w o r k R C 3环 境 中 运 行 通 过 多线程的概念 Windows 是一个多任务的系统,如果你使用的是 windows2000 及其以上版本,你可以通过任务管理器查看当前系统运行的程序和进程。什么是进程呢当一个程序开始运行时,它就是一个进程,进程所指包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的,线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。多线程是指程序中包含多个执行流,即在一个程序中可以同时运
2、行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。浏览器就是一个很好的多线程的例子,在浏览器中你可以在下载 JAVA小应用程序或图象的同时滚动页面,在访问新页面时,播放动画和声音,打印文件等。 多线程的好处在于可以提高 CPU 的利用率任何一个程序员都不希望自己的程序很多时候没事可干,在多线程程序中,一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。 然而我们也必须认识到线程本身可能影响系统性能的不利方面,以正确使用线程: 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多 多线程需要协调和管理,所以需
3、要 CPU 时间跟踪线程 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题 线程太多会导致控制太复杂,最终可能造成很多 Bug 基于以上认识,我们可以一个比喻来加深理解。假设有一个公司,公司里有很多各司其职的职员,那么我们可以认为这个正常运作的公司就是一个进程,而公司里的职员就是线程。一个公司至少得有一个职员吧,同理,一个进程至少包含一个线程。在公司里,你可以一个职员干所有的事,但是效率很显然是高不起来的,一个人的公司也不可能做大;一个程序中也可以只用一个线程去做事,事实上,一些过时的语言如 fortune,basic 都是如此,但是象一个人的公司一样,效率很低,如果做大程序,效
4、率更低事实上现在几乎没有单线程的商业软件。公司的职员越多,老板就得发越多的薪水给他们,还得耗费大量精力去管理他们,协调他们之间的矛盾和利益;程序也是如此,线程越多耗费的资源也越多,需要 CPU 时间去跟踪线程,还得解决诸如死锁,同步等问题。总之,如果你不想你的公司被称为“皮包公司”,你就得多几个员工;如果你不想让你的程序显得稚气,就在你的程序里引入多线程吧! 本文将对 C#编程中的多线程机制进行探讨,通过一些实例解决对线程的控制,多线程间通讯等问题。为了省去创建 GUI 那些繁琐的步骤,更清晰地逼近线程的本质,下面所有的程序都是控制台程序,程序最后的()是为了使程序中途停下来,以便看清楚执行过
5、程中的输出。 好了,废话少说,让我们来体验一下多线程的 C#吧! 操纵一个线程 任何程序在执行时,至少有一个主线程,下面这段小程序可以给读者一个直观的印象: ; ; publicclassSimple publicstaticintMain() (ThreadStart/Stop/JoinSample); AlphaoAlpha=newAlpha(); ThreadoThread=newThread(newThreadStart); (); while(!; (1); (); (); (); (); try (); (); catch(ThreadStateException) (.); (E
6、xpectedsinceabortedthreadscannotberestarted.); (); return0; 这段程序包含两个类 Alpha 和 Simple,在创建线程 oThread 时我们用指向()方法的初始化了 ThreadStart 代理(delegate)对象,当我们创建的线程 oThread 调用()方法启动时,实际上程序运行的是()方法: AlphaoAlpha=newAlpha(); ThreadoThread=newThread(newThreadStart); (); 然后在 Main()函数的 while 循环中, 我们使用静态方法()让主线程停了 1ms,
7、这段时间 CPU 转向执行线程 oThread。然后我们试图用()方法终止线程 oThread,注意后面的(),()方法使主线程等待, 直到 oThread线程结束。 你可以给()方法指定一个 int 型的参数作为等待的最长时间。之后,我们试图用()方法重新启动线程 oThread,但是显然 Abort()方法带来的后果是不可恢复的终止线程,所以最后程序会抛出 ThreadStateException 异常。 程序最后得到的结果将如下图: 在这里我们要注意的是其它线程都是依附于 Main()函数所在的线程的, Main()函数是 C#程序的入口,起始线程可以称之为主线程,如果所有的前台线程都停
8、止了,那么主线程可以终止,而所有的后台线程都将无条件终止。而所有的线程虽然在微观上是串行执行的,但是在宏观上你完全可以认为它们在并行执行。 读者一定注意到了这个属性,这个属性代表了线程运行时状态,在不同的情况下有不同的值, 于是我们有时候可以通过对该值的判断来设计程序流程。ThreadState 在各种情况下的可能取值如下: Aborted:线程已停止 AbortRequested:线程的()方法已被调用,但是线程还未停止 Background:线程在后台执行,与属性有关 Running:线程正在正常运行 Stopped:线程已经被停止 StopRequested:线程正在被要求停止 Susp
9、ended:线程已经被挂起(此状态下,可以通过调用 Resume()方法重新运行) SuspendRequested:线程正在要求被挂起,但是未来得及响应 Unstarted:未调用()开始线程的运行 WaitSleepJoin:线程因为调用了 Wait(),Sleep()或 Join()等方法处于封锁状态 上面提到了 Background 状态表示该线程在后台运行,那么后台运行的线程有什么特别的地方呢其实后台线程跟前台线程只有一个区别,那就是后台线程不妨碍程序的终止。一旦一个进程所有的前台线程都终止后,CLR(通用语言运行环境)将通过调用任意一个存活中的后台进程的 Abort()方法来彻底终
10、止进程。 当线程之间争夺 CPU 时间时,CPU 按照是线程的优先级给予服务的。在 C#应用程序中,用户可以设定 5 个不同的优先级,由高到低分别是 Highest,AboveNormal,Normal,BelowNormal,Lowest,在创建线程时如果不指定优先级,那么系统默认为。给一个线程指定优先级 ,我们可以使用如下代码: ame=(); for(inti=0;i10;i+) threadsi.Start(); (); 而多线程公用一个对象时,也会出现和公用代码类似的问题,这种问题就不应该使用 lock 关键字了,这里需要用到中的一个类 Monitor,我们可以称之为监视器,Moni
11、tor 提供了使线程共享资源的方案。 Monitor 类可以锁定一个对象, 一个线程只有得到这把锁才可以对该对象进行操作。 对象锁机制保证了在可能引起混乱的情况下一个时刻只有一个线程可以访问这个对象。Monitor必须和一个具体的对象相关联,但是由于它是一个静态的类,所以不能使用它来定义对象,而且它的所有方法都是静态的,不能使用对象来引用。下面代码说明了使用 Monitor 锁定一个对象的情形: . QueueoQueue=newQueue(); . (oQueue); . . . Produce:20 Consume:20 事实上,这个简单的例子已经帮助我们解决了多线程应用程序中可能出现的大
12、问题,只要领悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。 线程池和定时器多线程的自动管理 在多线程的程序中,经常会出现两种情况。一种情况下,应用程序中的线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应;而另外一种情况则是线程平常都处于休眠状态,只是周期性地被唤醒。在framework 里边,我们使用 ThreadPool 来对付第一种情况,使用Timer 来对付第二种情况。 ThreadPool 类提供一个由系统维护的线程池可以看作一个线程的容器,该容器需要Windows2000 以上版本的系统支持,因为其中某些方法调用了只有高版本的 Windows
13、才有的 API 函数。你可以使用()方法将线程安放在线程池里,该方法的原型如下: ookie); (=0, lock(HashCount) if ; = intiX=2000; (iX); ; W2K=false; if(W2K) for(intiItem=1;iItemMaxCount;iItem+) . . QueuetoThreadPool9 WaitingforThreadPooltodrain 980: =0, 1001: =1, 982: . . SettingeventX ThreadPoolhasbeendrained(Eventfired) Loadacrossthreads
14、 1012 1003 984 1021 与 ThreadPool 类不同,Timer 类的作用是设置一个定时器,定时执行用户指定的函数,而这个函数的传递是靠另外一个代理对象TimerCallback, 它必须在创建Timer对象时就指定,并且不能更改。定时器启动后,系统将自动建立一个新的线程,并且在这个线程里执行用户指定的函数。下面的语句初始化了一个 Timer 对象: Timertimer=newTimer(timerDelegate,s,1000,1000); 第一个参数指定了 TimerCallback 代理对象;第二个参数的意义跟上面提到的 WaitCallback 代理对象的一样,作
15、为一个传递数据的对象传递给要调用的方法;第三个参数是延迟时间计时开始的时刻距现在的时间, 单位是毫秒; 第四个参数是定时器的时间间隔计时开始以后,每隔这么长的一段时间,TimerCallback 所代表的方法将被调用一次,单位也是毫秒。这句话的意思就是将定时器的延迟时间和时间间隔都设为 1 秒钟。 定时器的设置是可以改变的,只要调用()方法,这是一个参数类型重载的方法,一般使用的原型如下: publicboolChange(long,long); 下面这段代码将前边设置的定时器修改了一下: (10000,2000); 很显然,定时器 timer 的时间间隔被重新设置为 2 秒,停止计时 10
16、秒后生效。 下面这段程序演示了 Timer 类的用法。 usingSystem; ; classTimerExampleState publicintcounter=0; publicTimertmr; classApp publicstaticvoidMain() TimerExampleStates=newTimerExampleState(); ; (); staticvoidCheckStatus(Objectstate) TimerExampleStates=(TimerExampleState)state; +; (0; if=5) .Change(10000,2000); (ch
17、anged.); if=10) (disposingoftimer.); =null; 程序首先创建了一个定时器, 它将在创建 1 秒之后开始每隔 1 秒调用一次 CheckStatus()方法,当调用 5 次以后,在 CheckStatus()方法中修改了时间间隔为 2 秒,并且指定在 10 秒后重新开始。当计数达到 10 次,调用()方法删除了 timer 对象,主线程于是跳出循环,终止程序。程序执行的结果如下: 上面就是对 ThreadPool 和 Timer 两个类的简单介绍,充分利用系统提供的功能,可以为我们省去很多时间和精力特别是对很容易出错的多线程程序。同时我们也可以看到Fram
18、ework 强大的内置对象,这些将对我们的编程带来莫大的方便。 互斥对象更加灵活的同步方式 有时候你会觉得上面介绍的方法好像不够用,对,我们解决了代码和资源的同步问题,解决了多线程自动化管理和定时触发的问题,但是如何控制多个线程相互之间的联系呢例如我要到餐厅吃饭,在吃饭之前我先得等待厨师把饭菜做好,之后我开始吃饭,吃完我还得付款,付款方式可以是现金,也可以是信用卡,付款之后我才能离开。分析一下这个过程,我吃饭可以看作是主线程,厨师做饭又是一个线程,服务员用信用卡收款和收现金可以看作另外两个线程,大家可以很清楚地看到其中的关系 我吃饭必须等待厨师做饭,然后等待两个收款线程之中任意一个的完成,然后
19、我吃饭这个线程可以执行离开这个步骤,于是我吃饭才算结束了。事实上,现实中有着比这更复杂的联系,我们怎样才能很好地控制它们而不产生冲突和重复呢? 这种情况下,我们需要用到互斥对象,即命名空间中的 Mutex 类。大家一定坐过出租车吧,事实上我们可以把 Mutex 看作一个出租车,那么乘客就是线程了,乘客首先得等车,然后上车,最后下车,当一个乘客在车上时,其他乘客就只有等他下车以后才可以上车。而线程与 Mutex 对象的关系也正是如此,线程使用()方法等待 Mutex 对象被释放,如果它等待的Mutex 对象被释放了,它就自动拥有这个对象,直到它调用()方法释放这个对象,而在此期间,其他想要获取这
20、个 Mutex 对象的线程都只有等待。 下面这个例子使用了 Mutex 对象来同步四个线程,主线程等待四个线程的结束,而这四个线程的运行又是与两个 Mutex 对象相关联的。 其中还用到 AutoResetEvent 类的对象, 如同上面提到的 ManualResetEvent 对象一样,大家可以把它简单地理解为一个信号灯,使用()方法可以设置它为有信号状态,而使用()方法把它设置为无信号状态。这里用它的有信号状态来表示一个线程的结束。 .); gM2=newMutex(true); (-MainOwnsgM1andgM2); AutoResetEventevs=newAutoResetEve
21、nt4; evs0=Event1; evs1=Event2; evs2=Event3; evs3=Event4; MutexSampletm=newMutexSample(); Threadt1=newThread(newThreadStart); Threadt2=newThread(newThreadStart); Threadt3=newThread(newThreadStart); Threadt4=newThread(newThreadStart); ();.MutexSample); (); publicvoidt1Start() (t1Startstarted,(Mutex);
22、MutexgMs=newMutex2; gMs0=gM1;/创建一个 Mutex 数组作为()方法的参数 gMs1=gM2; (gMs);/等待 gM1 和 gM2 都被释放 (2000); (t1Startfinished,(Mutex)satisfied); (); publicvoidt2Start() (t2Startstarted,(); ();/等待 gM1 的释放 (t2Startfinished,()satisfied); ();/线程结束,将 Event2 设置为有信号状态 publicvoidt3Start() (t3Startstarted,(Mutex); Mutexg
23、Ms=newMutex2; gMs0=gM1;/创建一个 Mutex 数组作为()方法的参数 gMs1=gM2; (gMs);/等待数组中任意一个 Mutex 对象被释放 (t3Startfinished,(Mutex); ();/线程结束,将 Event3 设置为有信号状态 publicvoidt4Start() (t4Startstarted,(); ();/等待 gM2 被释放 (t4Startfinished,(); ();/线程结束,将 Event4 设置为有信号状态 下面是该程序的执行结果: 从执行结果可以很清楚地看到, 线程 t2,t3 的运行是以 gM1 的释放为条件的, 而 t4 在 gM2释放后开始执行,t1 则在 gM1 和 gM2 都被释放了之后才执行。Main()函数最后,使用WaitHandle 等待所有的 AutoResetEvent 对象的信号,这些对象的信号代表相应线程的结束。 小结 多线程程序设计是一个庞大的主题,而本文试图在Framework 环境下,使用最新的 C#语言来描述多线程程序的概貌。希望本文能有助于大家理解线程这种概念,理解多线程的用途,理解它的 C#实现方法,理解线程将为我们带来的好处和麻烦。C#是一种新的语言,因此它的线程机制也有许多独特的地方,希望大家能通过本文清楚地看到这些,从而可以对线程进行更深入的理解和探索。