多线程(转)

  在当今的程序设计中,经常看到进程、线程这样的名词,其实它们属于一个更广大的范畴——多任务,现在让我们先从进程和线程切入,来领略一下这个领域的广阔风光。

  我了解不多,只能说说80x86上的Windows环境,其他如lpha,Mac或者Linux,我就一窍不通了,见笑见笑……

  现代操作系统都是多任务的操作系统,在这里要澄清一个概念,Windows 3.x时代也有所谓的多任务,但是,那并不是现代意义的多任务--"抢占式多任务"。Windows 3.x的多任务是非抢占式的,即,一个应用程序,甚至系统,要等现在正在运行的程序主动放弃CPU(程序员把这个礼貌的行为写到程序中),才能获得执行时间片。可以看出,在这种环境中,操作系统的主动性是很小的,只要某个已经获得CPU的程序不主动放弃CPU,操作系统就得不到时间运行,亦无法实行其管理调度功能。如果一个应用程序因崩溃而挂起,将导致整个系统挂起。

  而抢占式的多任务是指,将CPU时间片分配最需要它的应用程序,即便一个应用程序永远不打算放弃CPU,操作系统也能保证随时"抢占"CPU时间,然后对当前所有的任务进行合理的调度。

  要实现多任务,首先要有CPU的支持,80x86 (Alpha?我不懂,别问我……) 支持多任务,通过一个TSS--任务描述符,80x86能够描述一个任务。详细我就不说了,和本文关系不大,只要说明抢占式多任务不是操作系统在单干就行了。

  作为操作系统,要能够利用CPU提供的功能才能实现多任务,Windows做到了这点(当然,Linux也做到了,只是我不懂……)。系统有一个核心调度程序,负责为每一个任务分配CPU时间,允许其执行指定的一段时间,当这段时间用完后,控制权会重新交回到操作系统,操作系统可在此时重新分配CPU时间。至于CPU时间是如何分配的,就又不是本文的范围了。Microsoft给出的文档是说"系统保证CPU时间的分配是公平的",仅此而已……。

  在Windows中,一个任务就是一个线程,以前曾经说过,线程不能没有存储而存在,而其所依赖存在的存储对象就是进程。而一个进程--以前也说过了--对应一个映象(可执行文件)。也就是说,在一个可执行文件运行时,可以有多个线程。

  好了,基本的概念大概说明了,来讨论一下它们的用途。要这些多出来(除了原始线程外)的线程有什么用?MSDN给出的答案是"等"。但我个人认为稍微笼统了一点(偏激一点?)。不过,至少,有一点是肯定的--绝不要使多个线程同时进行繁重的操作--除非你是建立了CPU数目个线程(32路对等处理器系统?)。无论如何,实际工作的还是CPU,在这种情况下,CPU不仅不会少执行指令,还要执行很多的排班程序指令以及任务的切换指令--反而降低效率。

  在需要等待的地方,多线程确实能够发挥很高的效能。(注意,并不一定是最高,以后将说到经常是更好的实现方法,这里只是说明线程的用法)举一个例子:一个网络程序向远程主机发送了一个请求,正在等待回应,而在此期间,它还希望能够与用户进行交互。一种实现方法是:程序继续与用户交互,在交互的间歇检查一下回应是否到达。而更好的方法是建立一个新的线程(称为工作线程)来等待回应,原始线程继续照常与用户交互。如果您已经感觉到后一种方法确实比前一种方法好很多,那么,您可以不读下一段,不用听我罗嗦了。

  后一种方法比前一种方法好在后者执行的指令更少,因而效率更高。如果您对Windows编程熟悉:在实现前者时,必须保证能够及时检测到回应到达,因而就不能使用GetMessage()而要使用PeekMessage()。如果使用GetMessage(),而恰巧在很长的一段时间内都没有消息到达,原始线程就不会从GetMessage()返回,也就不能检测回应是否到达。使用PeekMessage(),可以令其在没有消息时也立即返回,因而可以检测回应是否到达。网络部分的情况也一样--程序不能等回应一直等下去否则就无法与用户交互。无论如何,在即没
有消息、也没有回应到达的情况下,原始线程没有有效的进入等待状态,而是不停地"空转",检测二者中是否有到达的,这对系统资源显然是极大的浪费。而对于后者,原始线程通过GetMessage()有效的进入了等待,工作线程也可以通过类似的方法进入等待,例如使用Socket的select()。

  但是,新的问题又出现了,工作线程发现回应已经到达了,它可能要通知原始线程才行--它与原始线程是并发执行的。这就涉及到线程简通信了,有一种最简单的方法:Windows消息。工作线程通过向原始线程的窗口发送一个消息,然后终止;当原始线程的消息循环发现这个消息时,就知道回应已经到达了。线程间通信的方法还有很多,将在以后专门介绍。

  最后要说明的是--很重要的一点:多线程经常不是程序员主动来使用的,而是在依赖操作系统时,已然是多线程了。即使用,也很有节制,滥用线程将适得其反。究竟什么地方应该使用多线程,并没有什么规则,将多线程用在该用的地方而已,当然不会刻意的使用它。当综合各种条件和各种可能的方案后,如发现多线程是最好的,就是使用多线程的最佳时机。我是否说的是废话?呵呵,其实就像这篇文章一样,我也很头疼,线程是在遇到实际需要时才想起用的,硬要设计一种情况来使用实在不易。有备无患。多线程不是杀手锏,但是掌握它是向较高级编程迈进的必经之路。

在当今的程序设计中,经常看到进程、线程这样的名词,其实它们属于一个更广大的范畴——多任务,现在让我们先从进程和线程切入,来领略一下这个领域的广阔风光。

  上次说了线程的作用,显然,要使得线程之间能够通讯才能利用好线程,否则,每个线程都各干个的,向一盘散沙,程序就什么也做不了儿了。现在,我们来看看线程间如何通讯。
  线程间通讯的方法有很多,常用的有:变量、临界段、Windows消息、事件。

  首先来讨论变量。既然线程都处于同一个进程内,它们的地址空间就是相同的,对于完全依赖地址空间的变量来说,当然可以被同在一个进程中的任意一个线程访问,因而就可以用来通讯。

  比如,有一个全局变量,两个线程;我们希望第一个线程在工作,而第二个线程等待,当第一个线程检测的某件事发生时,通知第二个线程,使第二个线程开始运行。可以将全局变量置为0,然后让第二个线程进入一个“死循环”,等待全局变量变为1。而第一个线程执行他的任务,当事件发生时,第一个线程将全局变量赋值为1,于是第二个线程便奇妙的结束了他的死循环,开始执行预定的工作。

  由此可见,变量确实可以进行线程间的通讯;但不是使用上面的方法(该方法被称为“循环锁”),因为它太拙劣了,第二个线程在等待中要消耗大量的CPU时间,却不作任何事。而且,它不能处理复杂的情况,例如:

  还是上两个线程,第一个线程分发任务,第二个线程执行任务;第一个线程每次给全局变量加上一个需执行的任务的数目,第二个线程每次从全局变量中减掉它完成的任务的数目。两个运算都是“全局变量 op 任务数目 → 全局变量”,假设线程一先读取全局变量,是n,然后加上新任务数目,结果是n+p;而就在同时,线程二也读取了全局变量,由于线程一还没来得及将结果写回,线程二读到的也是n,减去完成的任务数,结果是n-q;现在,两个线程都要将结果写回到全局变量,显然这里有问题,最终结果要么是n+p、要么是n-q,总之不是正确的n+p-q。

  问题就出在那个“同时”上,如果让线程二等线程一把结果写回全局变量再读取、减去、写回,错误就不会发生。

  Windows提供了一个专门针对变量通讯的同步方法——互锁函数。这是一组形如Interlocked???()的函数。每个函数都可以对一个变量进行一种特定的操作,在操作进行中,能够自动保持与其它线程同步——每个函数都执行一种“原子操作”——该操作不能与共用同一资源的其它操作同时进行。

  例如,上面的例子,通过使用InterlockedExchangeAdd()来执行加减操作,便可以保证对全局变量的操作不会同时发生。

  有时,线程间的通讯过程不像上述的那样简单、仅仅是做一次加减法,而是由许多步组成的。比如,线程一除了加上任务数外,还要填写每个任务的具体参数,线程一进而希望在自己填写完全部所需数据后,线程二再对它们修改,原因类似。这是可以使用临界段。

  CRITICAL_SECTION类型声明一个临界段结构变量,然后用InitializeCriticalSection()初始化它。EnterCriticalSection()和LeaveCriticalSection()两个API分别指示进入或退出临界段,参数是一个已经定义的临界段。在临界段内的一组操作都是原子的,它们不能与共用同一临界段的另外一组操作同时进行。

  与互锁函数的不同是,互锁函数仅涉及一个共享资源,执行一个操作,因而没有该保护哪些资源、保护多长时间的问题;临界段涉及多个资源,执行多个操作,因而需要一个变量来代表对一类资源和操作的保护。

  Windows的消息机制就是用来通讯的,它本身就支持线程间通讯。消息一般是基于窗口的,而窗口是属于创建它的线程的。两个属于不同线程的窗口可以通过Windows消息来通讯。例如上面线程二等待的情况就可以让线程二调用GetMessage()等待消息,当线程一使用PostMessage()将消息发送给线程二时,GetMessage()将返回,线程二可以执行相应的任务。消息的两个参数可以用来携带对任务的描述。即便一个线程没有窗口,其它线程也可以使用PostThreadMessage()将消息直接发给该线程。使用Windows消息来通讯,是一对一的进行的,在某些场合,可能需要一对多、多对一或多对多的通讯,这时,可以借助事件(Event)来完成。CreateEvent()创建一个事件,事件有两种状态——已触发和未触发。SetEvent()用来触发一个事件,ResetEvent()用来恢复事件到未触发状态。一组“等待函数”用来检测事件的状态,例如WaitForSingleObject()等待一个事件,直到事件的状态为已触发时才返回。与上面Windows消息的例子类似,它也可以用于让线程二等待,更方便的是,如果有另外的线程三做与线程二类似的工作,它可以等待同一个事件,线程一的一次SetEvent()将同时通知两个线程执行任务;如果有线程零做与线程一类似的工作,它也可以触发同一个事件,线程零或线程一任意一方触发事件都将使两个线程执行任务。

  除了上述的几种方法以外,旗语等方法也是用来进行线程间通讯,这里先不作介绍。

  当设计线程间的通讯时,一定要时刻记得各个线程之间是异步运行的,必须要使用某种机制才能进行通讯,在实际中,情况可能会非常复杂,如果考虑不全面,结果将是难以预料的。

你可能感兴趣的:(多线程(转))