用户模式的线程同步机制效率高,如果需要考虑线程同步问题,应该首先考虑用户模式的线程同步方法。但是,用户模式的线程同步有限制,对于多个进程之间的线程同步,用户模式的线程同步方法无能为力。这时,只能考虑使用内核模式。
Windows提供了许多内核对象来实现线程的同步。这些内核对象有两个非常重要的状态:“已通知”状态,“未通知”状态(也有翻译为:受信状态,未受信状态)。Windows提供了几种内核对象可以处于已通知状态和未通知状态:进程、线程、作业、文件、控制台输入/输出/错误流、事件、等待定时器、信号量、互斥对象。
与事件EVENT的配合使用,能够解决很多同步问题。
EVENT 的几个函数:
1、CreateEvent和OpenEvent
HANDLE WINAPI CreateEvent(
__in LPSECURITY_ATTRIBUTES lpEventAttributes, //表示安全控制,一般直接传入NULL,表示不能被子进程继承
__in BOOL bManualReset, //事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。
__in BOOL bInitialState, //Event的初始状态, TRUE为触发,FALSE未触发
__in LPCTSTR lpName //Event object的名字,NULL表示没名字(without a name)
);
要是CreateEvent创建的事件没名字 这个函数就没啥用了,不多做介绍,可查看msn。
HANDLE WINAPI OpenEvent( //获得已经存在的Event的事件句柄
__in DWORD dwDesiredAccess,
__in BOOL bInheritHandle,
__in LPCTSTR lpName //要打开的事件名字
);
2、SetEvent,触发事件
BOOL SetEvent(HANDLE hEvent);
3、ResetEvent,使事件状态设为未触发,如在创建事件时第二个参数为TRUE手动设置,则需要该函数去恢复事件为未触发状态。
BOOL ResetEvent(HANDLE hEvent);
4、PulseEvent, 如在创建事件时第二个参数为TRUE手动设置,其功能相当于SetEvent()后立即调用ResetEvent(),最好别用
BOOL PulseEvent(HANDLE hEvent)
5、CloseHandle(),关闭该句柄。
事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。
WaitForSingleObject()
在多线程下面,有时候我们会希望等待某一线程完成了再继续做其他事情,要实现这个目的,可以使用Windows API函数WaitForSingleObject,或者WaitForMultipleObjects。这两个函数都会等待Object被标为有信号(signaled)时才返回的。
那么,什么是信号呢?
简单来说,Windows下创建的Object都会被赋予一个状态量。如果Object被激活了,或者正在使用,那么该Object就是无信号,也就是不可用;另一方面,如果Object可用了,那么它就恢复有信号了。
这两个函数的优点是它们在等待的过程中会进入一个非常高效沉睡状态,只占用极少的CPU时间片。(这两个函数都是在内核状态下等待内核对象,不切换到用户模式下,因而效率很高)
1、格式
DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds);
如果想要等待线程,那么你需要指定线程的Handle,以及相应的Timeout时间。无限等待下去:INFINITE。
当dwMilliseconds为0时,测试hHandle核心对象是否被激发,函数立即返回。
2. 使用对象
它可以等待如下几种类型的对象:
Event(事件),Mutex(互斥量),Semaphore(信号量),Process(进程),Thread(线程),Change notification(变更通知),Console input(控制台输入),Job(可以被理解为进程的容器),Memory resource notification(内存资源通知),Waitable timer(等待定时器)
3. 返回类型
WAIT_ABANDONED:当hHandle为mutex时,如果拥有mutex的线程在结束时没有释放核心对象会引发此返回值。
WAIT_OBJECT_0:核心对象已被激活
WAIT_TIMEOUT:等待超时
WAIT_FAILED:出现错误,可通过GetLastError得到错误代码
4.示例:
#include <windows.h>
#include <stdio.h>
#include <iostream.h>
//声明函数 创建线程
DWORD WINAPI FunProc( LPVOID lpParameter);
void main()
{
HANDLE hThread;
hThread=CreateThread(NULL,0,FunProc,NULL,0,NULL);
DWORD dwRet=WaitForSingleObject(hThread, 1);
if(dwRet==WAIT_OBJECT_0)
{
printf("创建的线程执行结束\n");
}
if(dwRet==WAIT_TIMEOUT)
{
printf("等待超时\n");
}
if(dwRet==WAIT_ABANDONED)
{
printf("Abandoned\n");
}
CloseHandle(hThread);
}
DWORD WINAPI FunProc( LPVOID lpParameter )
{
int i=1;
for(; i<1000; i++)
{
printf("%d ", i);
if(! (i%10))
printf("\n");
}
return 0;
}
官方文档解释:
如果在wait操作仍处于暂挂状态时关闭此句柄,则函数的行为将不明确。
WaitForMultipleObjecct()
WaitForMultipleObjects是Windows中的一个功能非常强大的函数,几乎可以等待Windows中的所有的内核对象
函数原型为:
DWORD WaitForMultipleObjects(
DWORD nCount, // number of handles in the handle array
CONST HANDLE *lpHandles, // pointer to the object-handle array
BOOL fWaitAll, // wait flag
DWORD dwMilliseconds // time-out interval in milliseconds
);
参数解析:
DWORD 就是 Double Word, 每个word为2个字节的长度,DWORD双字即为4个字节,每个字节是8位。
nCount 指定列表中的句柄数量 最大值为MAXIMUM_WAIT_OBJECTS(64)
bWaitAll 等待的类型,如果为TRUE,表示除非对象都发出信号,否则就一直等待下去;如果FALSE,表示任何对象发出信号即可
函数的返回值有:
WAIT_ABANDONED_0:所有对象都发出消息,而且其中有一个或多个属于互斥体(一旦拥有它们的进程中止,就会发出信号)
WAIT_TIMEOUT:对象保持未发信号的状态,但规定的等待超时时间已经超过
WAIT_OBJECT_0:所有对象都发出信号
WAIT_IO_COMPLETION:(仅适用于WaitForMultipleObjectsEx)由于一个I/O完成操作已作好准备执行,所以造成了函数的返回
WAIT_FAILED则表示函数执行失败,会设置GetLastError
如bWaitAll为FALSE,那么返回结果相似,只是可能还会返回相对于WAIT_ABANDONED_0或WAIT_OBJECT_0的一个正偏移量,指出哪个对象是被抛弃还是发出信号。
WAIT_OBJECT_0是微软定义的一个宏,你就把它看成一个数字就可以了。
例如,WAIT_OBJECT_0 + 5的返回结果意味着列表中的第5个对象发出了信号
如果程序中的nObjectWait是WAIT_OBJECT_0 + 5
int nIndex = nObjectWait - WAIT_OBJECT_0;就是说nIndex =5也就表示第5个对象发出了信号
示例:
HANDLE m_hEvent[2];
//两事件
m_hEvent[0]=CreateEvent(NULL, FALSE, FALSE, NULL);
m_hEvent[1]=CreateEvent(NULL, FALSE, FALSE, NULL);
CreateThread(NULL, 0, MyThreadProc, this, 0, NULL);
DWORD WINAPI MyThreadProc(LPVOID lpParam)
{
while(TRUE)
{ //每次等500毫秒
int nIndex = WaitForMultipleObjects(2, pThis->m_hEvent, FALSE,500);
if (nIndex == WAIT_OBJECT_0 + 1)
{
//第二个事件发生 //ExitThread(0); //break;
}
else if (nIndex == WAIT_OBJECT_0) //第一个事件发生
{
//第一个事件
}
else if (nIndex == WAIT_TIMEOUT) //超时500毫秒
{ //超时可作定时用
}
}
OutputDebugString("线程结束. /n");
return 0L;}
当要处理第一个事件时,你只需执行SetEvent(m_hEvent[0]); 即可进入第一个事件的位置
当要执行第二个事件时执行SetEvent(m_hEvent[1]);
当 bWaitAll参数为TRUE等待所有的事件
DWORD WINAPI MyThreadProc(LPVOID lpParam)
{ while(TRUE)
{ //每次等500毫秒
int nIndex = WaitForMultipleObjects(2, pThis->m_hEvent, TRUE,500);
if (WAIT_OBJECT_0 + 1<= nIndex <= WAIT_OBJECT_0) //所有事件发生
{
//所有的信号量都有效时(事件都发生)其中之一无效。
}
------------------------------------------------------------------------------------------------------
注意:当WaitForMultipleObjects,如果它的bWaitAll 参数设置为false。其返回值减去WAIT_OBJECT_0 就是参数lpHandles数组的序号。如果同时有多个内核对象被触发,这个函数返回的只是其中序号最小的那个。
如何获取所有被同时触发的内核对象。
多个内核对象被触发时,WaitForMultipleObjects选择其中序号最小的返回。而WaitForMultipleObjects它只会改变使它返回的那个内核对象的状态。
这儿又会产生一个问题,如果序号最小的那个对象频繁被触发,那么序号比它大的内核对象将得不到被处理的机会。为了解决这一问题,可以采用双WaitForMultipleObjects检测机制来实现。见下面的例子:
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
DWORD dwRet = 0;
int nIndex = 0;
while(1)
{
dwRet = WaitForMultipleObjects(nCount,pHandles,false,INFINITE);
switch(dwRet)
{
case WAIT_TIMEOUT:
break;
case WAIT_FAILED:
return 1;
default:
{
nIndex = dwRet - WAIT_OBJECT_0;
ProcessHanlde(nIndex++); //同时检测其他的事件
while(nIndex < nCount) //nCount事件对象总数
{
dwRet = WaitForMultipleObjects(nCount - nIndex,&pHandles[nIndex],false,0);
switch(dwRet)
{
case WAIT_TIMEOUT:
nIndex = nCount; //退出检测,因为没有被触发的对象了.
break;
case WAIT_FAILED:
return 1;
default:
{
nIndex = dwRet - WAIT_OBJECT_0;
ProcessHanlde(nIndex++);
}
break;
}//switch结束
}//while结束
}//default结束
break;
}//switch结束
}//while结束
return 0;
}
SignalObjectAndWait()
你也可以同时通知一个内核对象,同时等待另一个内核对象,这两个操作以原子的方式进行。
语法
DWORD SignalObjectAndWait(
HANDLE hObjectToSignal,//通知的内核对象
HANDLE hObjectToWaitOn,//等待的内核对象
DWORD dwMilliseconds,//等待的时间
BOOL bAlertable//与IO完成端口有关的参数,暂不讨论
);
参数
hObjectToSignal
要发信号的对象的句柄。此对象可以是信号量,互斥量或事件。
如果句柄是信号量,则需要SEMAPHORE_MODIFY_STATE访问权限。如果句柄是事件,则需要EVENT_MODIFY_STATE访问权限。如果句柄是互斥锁且调用者不拥有互斥锁,则函数将失败并显示ERROR_NOT_OWNER。
hObjectToWaitOn
要等待的对象的句柄。该SYNCHRONIZE访问权是必需的; 有关更多信息,请参阅同步对象安全性和访问权限 。有关可以指定其句柄的对象类型的列表,请参阅“备注”部分。
dwMilliseconds
超时间隔,以毫秒为单位。即使对象的状态是非信号且没有完成或异步过程调用(APC)对象排队,该函数也会在间隔过去时返回。如果dwMilliseconds为零,则该函数测试对象的状态,检查排队的完成例程或APC,并立即返回。如果dwMilliseconds是INFINITE,则函数的超时间隔永远不会过去。
bAlertable
如果此参数为TRUE,则该函数在系统对I / O完成例程或APC函数进行排队时返回,并且该线程调用该函数。如果为FALSE,则函数不返回,并且线程不调用完成例程或APC函数。
当排队APC的函数调用完成时,完成例程排队。只有当bAlertable为TRUE且调用线程是排队APC的线程时,此函数才会返回并调用完成例程。
返回值
返回代码/值 描述
WAIT_ABANDONED/0x00000080L 指定的对象是在拥有线程终止之前由拥有互斥对象的线程未释放的互斥对象。互斥对象的所有权被授予调用线程,并且互斥锁被设置为无信号。如果互斥锁正在保护持久状态信息,则应检查它是否一致。
WAIT_IO_COMPLETION/0x000000C0L 等待由一个或多个排队到线程的用户模式 异步过程调用(APC)结束。
WAIT_OBJECT_0/0x00000000L 发出指定对象的状态信号。
WAIT_TIMEOUT/0x00000102L 超时间隔已过,对象的状态未发出信号。
WAIT_FAILED/(DWORD)0xFFFFFFFF 该功能失败了。要获取扩展错误信息,请调用 GetLastError。
备注
所述SignalObjectAndWait功能提供了一个更有效的方式来信号发送一个对象,然后在另一等待相比单独的函数调用诸如 SetEvent的随后WaitForSingleObject的。
该 SignalObjectAndWait函数可以等待以下对象:
更改通知
控制台输入
事件
内存资源通知
互斥
处理
信号
线
线程可以使用SignalObjectAndWait函数来确保工作线程在发信号通知对象之前处于等待状态。例如,线程和工作线程可以使用句柄来事件对象来同步它们的工作。该线程执行如下代码:
dwRet = WaitForSingleObject(hEventWorkerDone, INFINITE);
if( WAIT_OBJECT_0 == dwRet)
SetEvent(hEventMoreWorkToDo);
工作线程执行如下代码:
dwRet = SignalObjectAndWait(hEventWorkerDone,
hEventMoreWorkToDo,
INFINITE,
FALSE);
注意,“信号”和“等待”不能保证作为原子操作执行。在调用SignalObjectAndWait的线程开始等待第二个对象之前,在其他处理器上执行的线程可以观察第一个对象的信号状态。
在Windows 7中使用SignalObjectAndWait 和PulseEvent时要格外小心 ,因为在多个线程之间使用这些API会导致应用程序死锁。由信号发送线程SignalObjectAndWait 调用PulseEvent以发信号通知的等待对象SignalObjectAndWait呼叫。在某些情况下,SignalObjectAndWait的调用者无法及时接收等待对象的信号状态,从而导致死锁。
使用等待函数和直接或间接创建窗口的代码时要小心。如果一个线程创建了任何窗口,它必须处理消息。消息广播将发送到系统中的所有窗口。使用没有超时间隔的等待函数的线程可能会导致系统死锁。间接创建窗口的两个代码示例是DDE和COM CoInitialize。因此,如果您有一个创建窗口的线程,请务必从另一个线程调用SignalObjectAndWait。如果无法做到这一点,可以使用 MsgWaitForMultipleObjects或 MsgWaitForMultipleObjectsEx,但功能不相同。
要编译使用此函数的应用程序,请将 _WIN32_WINNT 定义为0x0400或更高版本。