原文地址:http://blog.csdn.net/morewindows/article/details/7445233(原文基础上有所修改)
上一篇中使用关键段来解决经典的多线程同步互斥问题,由于关键段的“线程所有权”特性所以关键段只能用于线程的互斥而不能用于同步。本篇介绍用事件Event来尝试解决这个线程同步问题。
首先介绍下如何使用事件。事件Event实际上是个内核对象,它的使用非常方便。下面列出一些常用的函数。
第一个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);
#include <stdio.h> #include <process.h> #include <windows.h> long g_nNum; unsigned int __stdcall Fun(void *pPM); const int THREAD_NUM = 10; //事件与关键段 HANDLE g_hThreadEvent; CRITICAL_SECTION g_csThreadCode; int main() { printf(" 经典线程同步 事件Event\n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); //初始化事件和关键段 自动置位,初始无触发的匿名事件 g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL); InitializeCriticalSection(&g_csThreadCode); HANDLE handle[THREAD_NUM]; g_nNum = 0; int i = 0; while (i < THREAD_NUM) { handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL); WaitForSingleObject(g_hThreadEvent, INFINITE); //等待事件被触发 i++; } //如果是WaitForSingleObject(g_hThreadEvent, INFINITE),则必须加上 WaitForMultipleObjects //如果是WaitForSingleObject(handle[i] INFINITE),则不需要加WaitForMultipleObjects WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); //销毁事件和关键段 CloseHandle(g_hThreadEvent); DeleteCriticalSection(&g_csThreadCode); return 0; } unsigned int __stdcall Fun(void *pPM) { int nThreadNum = *(int *)pPM; SetEvent(g_hThreadEvent); //触发事件 Sleep(50);//some work should to do EnterCriticalSection(&g_csThreadCode); g_nNum++; Sleep(0);//some work should to do printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum); LeaveCriticalSection(&g_csThreadCode); return 0; }
最后一个事件的清理与销毁
由于事件是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。
在经典多线程问题中设置一个事件和一个关键段。用事件处理主线程与子线程的同步,用关键段来处理各子线程间的互斥。详见代码:
#include <stdio.h> #include <process.h> #include <windows.h> long g_nNum; unsigned int __stdcall Fun(void *pPM); const int THREAD_NUM = 10; //事件与关键段 HANDLE g_hThreadEvent; CRITICAL_SECTION g_csThreadCode; int main() { printf(" 经典线程同步 事件Event\n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); //初始化事件和关键段 自动置位,初始无触发的匿名事件 g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL); InitializeCriticalSection(&g_csThreadCode); HANDLE handle[THREAD_NUM]; g_nNum = 0; int i = 0; while (i < THREAD_NUM) { handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL); WaitForSingleObject(g_hThreadEvent, INFINITE); //等待事件被触发 i++; } WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); //销毁事件和关键段 CloseHandle(g_hThreadEvent); DeleteCriticalSection(&g_csThreadCode); return 0; } unsigned int __stdcall Fun(void *pPM) { int nThreadNum = *(int *)pPM; SetEvent(g_hThreadEvent); //触发事件 Sleep(50);//some work should to do EnterCriticalSection(&g_csThreadCode); g_nNum++; Sleep(0);//some work should to do printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum); LeaveCriticalSection(&g_csThreadCode); return 0; }
运行结果如下图:
可以看出来,经典线线程同步问题已经圆满的解决了——线程编号的输出没有重复,说明主线程与子线程达到了同步。全局资源的输出是递增的,说明各子线程已经互斥的访问和输出该全局资源。
现在我们知道了如何使用事件,但学习就应该要深入的学习,何况微软给事件还提供了PulseEvent()函数,所以接下来再继续深挖下事件Event,看看它还有什么秘密没。
先来看看这个函数的原形:
第五个PulseEvent
函数原型:BOOLPulseEvent(HANDLEhEvent);
在MSDN上解释如下:
This function provides a single operation that sets to signaled the state of the specified event object and then resets it to nonsignaled after releasing the appropriate number of waiting threads(这个函数将特定事件设置为有信号状态,当释放掉相关的线程后,在将事件设置为无信号状态)
此时情况可以分为两种:1.对于手动置位事件,由于该事件调用WaitForSingleObject()后不会自动调用ResetEvent()使事件变成未触发状态,所以,所有正处于等待状态下线程都变成可调度状态。
2.对于自动置位事件,由于该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态,所以,所有正处于等待状态下线程只有一个变成可调度状态。
此后事件是末触发的。
在使用PulseEvent()的时候却发现,等待的线程并不能都被激活,那么,究竟发生了什么呢?
看下面的代码,你是否以为会如你所愿的进行?
SignalSemaphore(hOtherSemaphore);
WaitForSingleObject(hEvent, INFINITE);
事实上,这并不总是正确的。因为在信号和等待之间存在竞争,如果在一个线程PluseEvent()的时候,恰好没有线程处于等待状态,那么这个事件将丢失!
这是我在网上搜到的一段话:
While the thread is sitting waiting for the event, a device driver or part of the kernel itself might ask to borrow the thread to do some processing (by means of a "kernel-mode APC"). During that time, the thread isnot in the wait state. (It's being used by the device driver.) If thePulseEvent
happens while the thread is being "borrowed", then it willnot be woken from the wait, because thePulseEvent
function wakes only threads that were waitingat the time thePulseEvent
occurs.
很好的解释了这种现象的原因,原来一个线程处于等待状态的过程中,会有一些瞬间其状态并不为“等待状态”。这就造成PulseEvent()不能激活应该被激活的等待线程。
这些瞬间包括什么呢?比如发生了系统调用,缺页中断,硬件中断......等等。
怎么解决这个问题呢?MSDN建议使用SiginalObjectAndWait()。具体信息可以查询MSDN.
下面对这个触发一个事件脉冲PulseEvent ()写一个例子,主线程启动7个子线程,其中有5个线程Sleep(10)后对一事件调用等待函数(称为快线程),另有2个线程Sleep(100)后也对该事件调用等待函数(称为慢线程)。主线程启动所有子线程后再Sleep(50)保证有5个快线程都正处于等待状态中。此时若主线程触发一个事件脉冲,那么对于手动置位事件,这5个线程都将顺利执行下去。对于自动置位事件,这5个线程中会有中一个顺利执行下去。而不论手动置位事件还是自动置位事件,那2个慢线程由于Sleep(100)所以会错过事件脉冲,因此慢线程都会进入等待状态而无法顺利执行下去。
代码如下:
#include <stdio.h>
#include <process.h>
#include <windows.h>
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//事件与关键段
HANDLE g_hThreadEvent;
CRITICAL_SECTION g_csThreadCode;
int main()
{
printf(" 经典线程同步 事件Event\n");
printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
//初始化事件和关键段 自动置位,初始无触发的匿名事件
g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
InitializeCriticalSection(&g_csThreadCode);
HANDLE handle[THREAD_NUM];
g_nNum = 0;
int i = 0;
while (i < THREAD_NUM)
{
handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
WaitForSingleObject(g_hThreadEvent, INFINITE); //等待事件被触发
i++;
}
WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
//销毁事件和关键段
CloseHandle(g_hThreadEvent);
DeleteCriticalSection(&g_csThreadCode);
return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
int nThreadNum = *(int *)pPM;
SetEvent(g_hThreadEvent); //触发事件
Sleep(50);//some work should to do
EnterCriticalSection(&g_csThreadCode);
g_nNum++;
Sleep(0);//some work should to do
printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum);
LeaveCriticalSection(&g_csThreadCode);
return 0;
}
对自动置位事件,运行结果如下:
对手动置位事件,运行结果如下:
最后总结下事件Event
1.事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。
2.事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。
3.事件可以解决线程间同步问题,因此也能解决互斥问题,代码如下:
#include <stdio.h>
#include <process.h>
#include <windows.h>
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//事件与关键段
HANDLE g_hThreadEvent;
CRITICAL_SECTION g_csThreadCode;
int main()
{
printf(" 经典线程同步 事件Event\n");
//初始化事件和关键段 自动置位,初始无触发的匿名事件
g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
HANDLE handle[THREAD_NUM];
g_nNum = 0;
int i = 0;
while (i < THREAD_NUM)
{
handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
WaitForSingleObject(g_hThreadEvent, INFINITE); //等待事件被触发
i++;
}
WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
//销毁事件和关键段
CloseHandle(g_hThreadEvent);
//DeleteCriticalSection(&g_csThreadCode);
return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
int nThreadNum = *(int *)pPM;
SetEvent(g_hThreadEvent); //触发事件
g_nNum++;
//Sleep(50);//some work should to do
printf("线程编号为%d 全局变量为%d\n", nThreadNum,g_nNum);
return 0;
}
运行结果如下: