1、 Event 用事件(Event)来同步线程是最具弹性的了。一个事件有两种状态:激发状态和未激发状态。也称有信号状态和无信号状态。事件又分两种类型:手动重置事件和自动重置事件。手动重置事件被设置为激发状态后,会唤醒所有等待的线程,而且一直保持为激发状态,直到程序重新把它设置为未激发状态。自动重置事件被设置为激发状态后,会唤醒“一个”等待中的线程,然后自动恢复为未激发状态。所以用自动重置事件来同步两启汪胡个线程比较理想。MFC中对应的类为CEvent.。CEvent的构造函数默认创建一个自动重置的事件,而且处于未激发状态。共有三个函数来改变事件的状态:SetEvent,ResetEvent和PulseEvent。用事件来同步线程是一种比较理想的做法,但在实际的使用过程中要注意的是,对自动重置事件调用SetEvent和PulseEvent有可能会引起死锁,必须小心。 多线程同步-event 在所有的内核对象中,事件内核对象是个最基本的。它包含一个使用计数(与所有内核对象一样),一个BOOL值(用悄拦于指明该事件是个自动重置的事件还是一个人工重置的事件),还有一个BOOL值(用于指明该事件处于已通知状态还是未通知状态)。事件能够通知一个线程的操作已经完成。有两种类型的事件对象。一种是人工重置事件,另一种是自动重置事件。他们不同的地方在于:当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。 当一个线程执行初始化操作,然后通知另一个线程执行剩余的操作时,事件使用得最频繁。在这种情况下,事件初始化为未通知状态,然后,当该线程完成它的初始化操作后,它就将事件设置为已通知状态,而一直在等待该事件的另一个线程在事件已经被通知后,就变成可调度线程。 当这个进程启动时,它创建一个人工重置的未通知状态的事件,并且将句柄保存在一个全局变量中。这使得该进程中的其他线程能够非常容易地访问同一个事件对象。程序一开始创建了三个线程,这些线程在初始化后就被挂起,等待事件。这些线程要等待文件的内容读入内存,然陵罩后每个线程都会访问这段文件内容。一个线程进行单词计数,另一个线程运行拼写检查,第三个线程运行语法检查。这3个线程函数的代码的开始部分都相同,每个函数都调用WaitForSingleObject.,这将使线程暂停运行,直到文件的内容由主线程读入内存为止。一旦主线程将数据准备好,它就调用SetEvent,给事件发出通知信号。这时,系统就使所有这3个辅助线程进入可调度状态,它们都获得了C P U时间,并且可以访问内存块。这3个线程都必须以只读方式访问内存,否则会出现内存错误。这就是所有3个线程能够同时运行的唯一原因。如果计算机上配有三个以上CPU,理论上这个3个线程能够真正地同时运行,从而可以在很短的时间内完成大量的操作 如果你使用自动重置的事件而不是人工重置的事件,那么应用程序的行为特性就有很大的差别。当主线程调用S e t E v e n t之后,系统只允许一个辅助线程变成可调度状态。同样,也无法保证系统将使哪个线程变为可调度状态。其余两个辅助线程将继续等待。已经变为可调度状态的线程拥有对内存块的独占访问权。 让我们重新编写线程的函数,使得每个函数在返回前调用S e t E v e n t函数(就像Wi n M a i n函数所做的那样)。 当主线程将文件内容读入内存后,它就调用SetEvent函数,这样操作西永就会使这三个在等待的线程中的一个成为可调度线程。我们不知道系统将首先选择哪个线程作为可调度线程。当该线程完成操作时,它也将调用S e t E v e n t函数,使下一个被调度。这样,三个线程会以先后顺序执行,至于什么顺序,那是操作系统决定的。所以,就算每个辅助线程均以读/写方式访问内存块,也不会产生任何问题,这些线程将不再被要求将数据视为只读数据。 这个例子清楚地展示出使用人工重置事件与自动重置事件之间的差别。 P u l s e E v e n t函数使得事件变为已通知状态,然后立即又变为未通知状态,这就像在调用S e t E v e n t后又立即调用R e s e t E v e n t函数一样。如果在人工重置的事件上调用P u l s e E v e n t函数,那么在发出该事件时,等待该事件的任何一个线程或所有线程将变为可调度线程。如果在自动重置事件上调用P u l s e E v e n t函数,那么只有一个等待该事件的线程变为可调度线程。如果在发出事件时没有任何线程在等待该事件,那么将不起任何作用[2]。 2、 Critical Section 使用临界区域的第一个忠告就是不要长时间锁住一份资源。这里的长时间是相对的,视不同程序而定。对一些控制软件来说,可能是数毫秒,但是对另外一些程序来说,可以长达数分钟。但进入临界区后必须尽快地离开,释放资源。如果不释放的话,会如何?答案是不会怎样。如果是主线程(GUI线程)要进入一个没有被释放的临界区,呵呵,程序就会挂了!临界区域的一个缺点就是:Critical Section不是一个核心对象,无法获知进入临界区的线程是生是死,如果进入临界区的线程挂了,没有释放临界资源,系统无法获知,而且没有办法释放该临界资源。这个缺点在互斥器(Mutex)中得到了弥补。Critical Section在MFC中的相应实现类是CcriticalSection。CcriticalSection::Lock()进入临界区,CcriticalSection::UnLock()离开临界区。 3、 Mutex 互斥器的功能和临界区域很相似。区别是:Mutex所花费的时间比Critical Section多的多,但是Mutex是核心对象(Event、Semaphore也是),可以跨进程使用,而且等待一个被锁住的Mutex可以设定TIMEOUT,不会像Critical Section那样无法得知临界区域的情况,而一直死等。MFC中的对应类为CMutex。Win32函数有:创建互斥体CreateMutex() ,打开互斥体OpenMutex(),释放互斥体ReleaseMutex()。Mutex的拥有权并非属于那个产生它的线程,而是最后那个对此Mutex进行等待操作(WaitForSingleObject等等)并且尚未进行ReleaseMutex()操作的线程。线程拥有Mutex就好像进入Critical Section一样,一次只能有一个线程拥有该Mutex。如果一个拥有Mutex的线程在返回之前没有调用ReleaseMutex(),那么这个Mutex就被舍弃了,但是当其他线程等待(WaitForSingleObject等)这个Mutex时,仍能返回,并得到一个WAIT_ABANDONED_0返回值。能够知道一个Mutex被舍弃是Mutex特有的。 4、 Semaphore 信号量是最具历史的同步机制。信号量是解决producer/consumer问题的关键要素。对应的MFC类是Csemaphore。Win32函数CreateSemaphore()用来产生信号量。ReleaseSemaphore()用来解除锁定。Semaphore的现值代表的意义是目前可用的资源数,如果Semaphore的现值为1,表示还有一个锁定动作可以成功。如果现值为5,就表示还有五个锁定动作可以成功。当调用Wait…等函数要求锁定,如果Semaphore现值不为0,Wait…马上返回,资源数减1。当调用ReleaseSemaphore()资源数加1,当时不会超过初始设定的资源总数。