首先介绍下如何使用事件。事件Event实际上是个内核对象,它的使用非常方便。下面列出一些常用的函数。




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]



第一个CreateEvent

函数功能:创建事件

函数原型:

HANDLECreateEvent(

LPSECURITY_ATTRIBUTESlpEventAttributes,

BOOLbManualReset,

BOOLbInitialState,

LPCTSTRlpName

);

函数说明:

第一个参数表示安全控制,一般直接传入NULL

第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。打个小小比方,手动置位事件相当于教室门,教室门一旦打开(被触发),所以有人都可以进入直到老师去关上教室门(事件变成未触发)。自动置位事件就相当于医院里拍X光的房间门,门打开后只能进入一个人,这个人进去后会将门关上,其它人不能进入除非门重新被打开(事件重新被触发)。

第三个参数表示事件的初始状态,传入TRUR表示已触发。

第四个参数表示事件的名称,传入NULL表示匿名事件。

第二个OpenEvent

函数功能:根据名称获得一个事件句柄。

函数原型:

HANDLEOpenEvent(

DWORDdwDesiredAccess,

BOOLbInheritHandle,

LPCTSTRlpName     //名称

);

函数说明:

第一个参数表示访问权限,对事件一般传入EVENT_ALL_ACCESS。详细解释可以查看MSDN文档。

第二个参数表示事件句柄继承性,一般传入TRUE即可。

第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个事件。

第三个SetEvent

函数功能:触发事件

函数原型:BOOLSetEvent(HANDLEhEvent);

函数说明:每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态。

第四个ResetEvent

函数功能:将事件设为末触发

函数原型:BOOLResetEvent(HANDLEhEvent);

最后一个事件的清理与销毁

由于事件是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。

在经典多线程问题中设置一个事件和一个关键段。用事件处理主线程与子线程的同步,用关键段来处理各子线程间的互斥。详见代码:

  1. #include <stdio.h>

  2. #include <process.h>

  3. #include <windows.h>

  4. long g_nNum;  

  5. unsigned int __stdcall Fun(void *pPM);  

  6. constint THREAD_NUM = 10;  

  7. //事件与关键段

  8. HANDLE  g_hThreadEvent;  

  9. CRITICAL_SECTION g_csThreadCode;  

  10. int main()  

  11. {  

  12.    printf("     经典线程同步 事件Event\n");  

  13.    printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  

  14. //初始化事件和关键段 自动置位,初始无触发的匿名事件

  15.    g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);  

  16.    InitializeCriticalSection(&g_csThreadCode);  

  17. HANDLE  handle[THREAD_NUM];  

  18.    g_nNum = 0;  

  19. int i = 0;  

  20. while (i < THREAD_NUM)  

  21.    {  

  22.        handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);  

  23.        WaitForSingleObject(g_hThreadEvent, INFINITE); //等待事件被触发

  24.        i++;  

  25.    }  

  26.    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  

  27. //销毁事件和关键段

  28.    CloseHandle(g_hThreadEvent);  

  29.    DeleteCriticalSection(&g_csThreadCode);  

  30. return 0;  

  31. }  

  32. unsigned int __stdcall Fun(void *pPM)  

  33. {  

  34. int nThreadNum = *(int *)pPM;  

  35.    SetEvent(g_hThreadEvent); //触发事件

  36.    Sleep(50);//some work should to do

  37.    EnterCriticalSection(&g_csThreadCode);  

  38.    g_nNum++;  

  39.    Sleep(0);//some work should to do

  40.    printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);  

  41.    LeaveCriticalSection(&g_csThreadCode);  

  42. return 0;  

  43. }  

运行结果如下图:

秒杀多线程第六篇 经典线程同步 事件Event _秒杀

可以看出来,经典线线程同步问题已经圆满的解决了——线程编号的输出没有重复,说明主线程与子线程达到了同步。全局资源的输出是递增的,说明各子线程已经互斥的访问和输出该全局资源。

现在我们知道了如何使用事件,但学习就应该要深入的学习,何况微软给事件还提供了PulseEvent()函数,所以接下来再继续深挖下事件Event,看看它还有什么秘密没。

先来看看这个函数的原形:

第五个PulseEvent

函数功能:将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲。

函数原型:BOOLPulseEvent(HANDLEhEvent);

函数说明:这是一个不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent();此时情况可以分为两种:

1.对于手动置位事件,所有正处于等待状态下线程都变成可调度状态。

2.对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。

此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态

下面对这个触发一个事件脉冲PulseEvent ()写一个例子,主线程启动7个子线程,其中有5个线程Sleep(10)后对一事件调用等待函数(称为快线程),另有2个线程Sleep(100)后也对该事件调用等待函数(称为慢线程)。主线程启动所有子线程后再Sleep(50)保证有5个快线程都正处于等待状态中。此时若主线程触发一个事件脉冲,那么对于手动置位事件,这5个线程都将顺利执行下去。对于自动置位事件,这5个线程中会有中一个顺利执行下去。而不论手动置位事件还是自动置位事件,那2个慢线程由于Sleep(100)所以会错过事件脉冲,因此慢线程都会进入等待状态而无法顺利执行下去。

代码如下:

  1. //使用PluseEvent()函数

  2. #include <stdio.h>

  3. #include <conio.h>

  4. #include <process.h>

  5. #include <windows.h>

  6. HANDLE  g_hThreadEvent;  

  7. //快线程

  8. unsigned int __stdcall FastThreadFun(void *pPM)  

  9. {  

  10.    Sleep(10); //用这个来保证各线程调用等待函数的次序有一定的随机性

  11.    printf("%s 启动\n", (PSTR)pPM);  

  12.    WaitForSingleObject(g_hThreadEvent, INFINITE);  

  13.    printf("%s 等到事件被触发 顺利结束\n", (PSTR)pPM);  

  14. return 0;  

  15. }  

  16. //慢线程

  17. unsigned int __stdcall SlowThreadFun(void *pPM)  

  18. {  

  19.    Sleep(100);  

  20.    printf("%s 启动\n", (PSTR)pPM);  

  21.    WaitForSingleObject(g_hThreadEvent, INFINITE);  

  22.    printf("%s 等到事件被触发 顺利结束\n", (PSTR)pPM);  

  23. return 0;  

  24. }  

  25. int main()  

  26. {  

  27.    printf("  使用PluseEvent()函数\n");  

  28.    printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  

  29. BOOL bManualReset = FALSE;  

  30. //创建事件 第二个参数手动置位TRUE,自动置位FALSE

  31.    g_hThreadEvent = CreateEvent(NULL, bManualReset, FALSE, NULL);  

  32. if (bManualReset == TRUE)  

  33.        printf("当前使用手动置位事件\n");  

  34. else

  35.        printf("当前使用自动置位事件\n");  

  36. char szFastThreadName[5][30] = {"快线程1000", "快线程1001", "快线程1002", "快线程1003", "快线程1004"};  

  37. char szSlowThreadName[2][30] = {"慢线程196", "慢线程197"};  

  38. int i;  

  39. for (i = 0; i < 5; i++)  

  40.        _beginthreadex(NULL, 0, FastThreadFun, szFastThreadName[i], 0, NULL);  

  41. for (i = 0; i < 2; i++)  

  42.        _beginthreadex(NULL, 0, SlowThreadFun, szSlowThreadName[i], 0, NULL);  

  43.    Sleep(50); //保证快线程已经全部启动

  44.    printf("现在主线程触发一个事件脉冲 - PulseEvent()\n");  

  45.    PulseEvent(g_hThreadEvent);//调用PulseEvent()就相当于同时调用下面二句

  46. //SetEvent(g_hThreadEvent);

  47. //ResetEvent(g_hThreadEvent);

  48.    Sleep(3000);  

  49.    printf("时间到,主线程结束运行\n");  

  50.    CloseHandle(g_hThreadEvent);  

  51. return 0;  

  52. }  

自动置位事件,运行结果如下:

秒杀多线程第六篇 经典线程同步 事件Event _多线程_02

手动置位事件,运行结果如下:

秒杀多线程第六篇 经典线程同步 事件Event _经典_03

最后总结下事件Event

1.事件是内核对象,事件分为手动置位事件自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。

2.事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。

3.事件可以解决线程间同步问题,因此也能解决互斥问题。

后面二篇《秒杀多线程第七篇 经典线程同步 互斥量Mutex》和《秒杀多线程第八篇 经典线程同步 信号量Semaphore》将介绍如何使用互斥量和信号量来解决这个经典线程同步问题。欢迎大家继续秒杀多线程之旅。