上次说了线程的作用,显然,要使得线程之间能够通讯才能利用好线程,否则,每个线程都各干个的,向一盘散沙,程序就什么也做不了儿了。现在,我们来看看线程间如何通讯。
线程间通讯的方法有很多,常用的有:变量、临界段、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()将同时通知两个线程执行任务;如果有线程零做与线程一类似的工作,它也可以触发同一个事件,线程零或线程一任意一方触发事件都将使两个线程执行任务。
除了上述的几种方法以外,旗语等方法也是用来进行线程间通讯的,这里先不作介绍。
当设计线程间的通讯时,一定要时刻记得各个线程之间是异步运行的,必须要使用某种机制才能进行通讯,在实际中,情况可能会非常复杂,如果考虑不全面,结果将是难以预料的。