本章讨论的是如何使用内核对象来对线程进行同步,与用户模式下的同步机制相比,内核对象的用途要广泛的多。实际上,内核对象唯一的缺点就是他们的性能。当我们调用本章任何一个新函数时,调用线程必须从用户模式切换到内核模式,这种切换是比较耗时的。对于线程同步来说,这些内核对象中的每一种要么处于触发状态,要么处于未触发状态。微软为每种对象创建了一些规则,规定如何在这两种状态之间进行转换。例如:进程内核对象在创建的时候总是未触发状态的。当进程终止的情况,操作系统会自动使进程内核对象变为触发状态。当进程内核对象被触发后,它将永远保持这种状态,再也不会变回到未触发状态。
下面的内核对象既可以出发触发状态,也可以未触发状态:
进程 线程 作业 文件以及控制台的标准输入/输出/错误流 事件 可等待的计时器 信号量 互斥量
线程可以自己切换到等待状态,直到另一个对象被触发为止。windows提供了专门用来帮助我们进行线程同步的内核对象,事件 可等待计时器 信号量以及互斥量。
等待函数使一个线程资源进入等待状态,直到制定的内核对象被触发为止。注意,如果线程在调用一个等待函数的时候,相应的内核对象已经出发触发状态,那么线程是不会进入等待状态的。
最常用的是WaitForSingleObject:
DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);当线程调用这函数的时候,第一个参数hObject用来标识要等待的内核对象,这内核对象可以处于触发状态或未触发状态。第二个参数用来指定线程最多愿意花多长的时间来等待对象被触发。当第二个参数设置为INFINITE,表示线程愿意永远等待。如果内核对象永远不被触发,那么调用线程永远不会被唤醒。 DWORD dw=WaitForSingleObject(hProcess,5000); switch(dw) { cse WAIT_OBJECT_0://规定的时间内等待成功。 break; case WAIT_TIMEOUT://超时。 break; case WAIT_FAILED://指定的句柄无效。 break; Default: break; }WaitForSingleObject的返回值表示为什么调用线程又能够继续执行了。如果线程等待的对象被触发,那么返回值是WAIT_OBJECT_0,如果是因为等待超时,那么返回值是WAIT_TIMEOUT,如果传入了一个无效参数,那么返回值是WAIT_FAILED。(这时候可以用GetLastError得到更多的信息)
WaitForMultipleObjects()允许你在同一时间等待一个以上的对象。
DWORD WaitForMultipleObject( DWORD nCount, //表示lphandles数组的元素个数 CONST HANDLE *lphandles, //指向一个有对象handles组成的数组。 BOOL bWaitAll, //如果为true,则表示所有的handles都必须激发,此函数才得以返回,否则,此函数将在任何一个handle激发时就返回。 DWORD dwMilliseconds //超时也返回,可为0,立即返回,INFINITE表示无穷等待。 );有两种不同的方式来使用,一种是让线程进入等待状态直到指定内核对象的一个被触发为止,另一种是让线程进入等待状态直到制定内核对象中的全部被触发为止。参数bWaitAll用来告诉我们希望使用哪种方式,如果给这参数传TRUE,那么在所有内核对象被触发之前,函数将不会允许调用线程继续执行。
下面有使用的伪代码
HANDLE h[3]; H[0]=hProcess1; H[1=hProcess2; H[2]=hProcess3; DWORD dw=WaitForMultipleOBjecs(3,H,false,5000); switch(dw) { case WAIT_OBJEC_0://第一个对象被触发。 break; case WAIT_OBJEC_0+1://第二个对象被触发。 break; case WAIT_OBJEC_0+2://第三个对象被触发。 break; case WAIT_TIMEOUT://超时 break; case WAIT_FAILED://句柄无效。 break; }事件内核对象:包含一个使用技术,一个用来表示事件是自动重置事件还是自动重置时间的布尔值,以及用来表示事件有没有被触发的布尔值。 事件的触发表示一个操作已经完成。有两种不同类型的事件对象,手动重置对象和自动重置对象。当一个手动重置对象被触发的时候,正在等待该事件的所有线程都将变成可调度状态。而当一个自动重置事件被触发时,只有一个正在等待该事件的县城会变成可调度状态。
事件最通常的用途是:让一个线程执行初始化工作,然后触发另一个线程,让他执行剩余的工作,一开始的时候我们将时间初始化为未触发状态,然后当线程完成初始化工作的时候触发事件。此时,另一个线程一直在等待该事件,它发现事件被触发,于是变成可调度状态,第二个线程知道第一个线程已经完成了他的工作。
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes,// 安全属性 BOOLb ManualReset,// 复位方式 BOOLb InitialState,// 初始状态 LPCTSTR lpName // 对象名称 );参数bManualReset是一个布尔值,用来告诉系统应该创建一个手动重置时间(TRUE)还是一个自动重置时间(FALSE)。参数bInitialState表示应该事件初始化为触发状态(TRUE)还是未触发状态(FALSE)。当系统插件事件对象之后,返回一个事件内核对象。还有一个是CreateEvent的增强版CreateEventEx。
其他进程中的线程可以通过多种方式来访问该事件对象,这包括调用CreateEvent并在pszName参数中传入相同的值,使用继承,使用DuplicateHandle函数或者调用OpenEvent并在pszName参数指定与CreateEvent中相同的名字:
HANDLE OpenEvent( DWORD dwDesiredAccess, BOOL bInherit, PCTSTR pszName ); 一旦我们不需要事件内核对象的时候调用closehandlel来关闭。通过调用SetEvent可以直接控制它的状态,变成触发状态,使用ResetEvent使得事件变成未触发状态。 BOOL SetEvent(HANDLE hEvent); BOOL ResetEvent(HANDLE hEvent);使用例子
HANDLE g_hEvent; int main() { g_hEvent=CreateEvent(NULL,true,false,NULL); HANDLE hThread=CreateThread NULL,0,Thread1,NULL,0,NULL); //打开文件并读取内存。 SetEvent(g_hEvent);//通知Thread1开始运行。 //其他操纵。 } DWORD WINAPI Thread1(PVOID param) { WatiForSingleObject(g_hEvent,INFINITE); //访问内存。 ResetEvent(g_hEvent); }可等待的计时器内核对象:内核对象,他会在某个指定的时间触发或每隔一段时间触发一次,他们通常用来在某个时间执行一些操作。
要创建可等待的计时器,调用CreateWaitableTimer。
HANDLE CreateWaitableTimer( PSECURITY_ATTRIBUTES psa, BOOL bManualReset, PCTSTR pszName);bManualReset表示要创建的是一个手动重置计时器还是自动重置计时器。当手动重置计时器被触发,正在等待该计时器的所有线程都变成可调度状态。当自动重置计时器被触发时,只有一个等待该计时器的线程变成可调度状态。
进程还可以通过OpenWaitableTimer函数来得到一个已经存在的可等待计时器的句柄,该句柄和当前进程相关联。
HANDLE OPenWaitableTimer( DWORD dwDesiredAceess, BOOL bInheritHandle, PCTSTR pszName);在创建的时候,可等待的计时器对象总是处于未触发状态,当我们想要触发计时器的时候,必须调用SetWaitableTimer.
BOOL SetWaitableTimer( HANDLE hTimer, Const LARGE_INTEGEr*pDueTime, LONG lPeriod, PTIMERRAPCROUTING pfnCompletionRoutine, PVOID pvArgToCompletionRoutine, BOOL bResume); hTimer表示我们要触发的计时器。pDueTime和lPeriod要配合使用。pDueTime表示计时器第一次触发的时间。lPeriod表示在第一次触发之后,计时器的触发频度,它们都是以毫秒为单位。取消定时器CancelWaitableTimer.
BOOL CancelWaitableTimer(HANDLE hTimer);用户计时器 相信大家在使用VC时一定接触过用户计时器。它通过SetTimer来设置。但是好多人对它与可等待计时器混淆不清。其实,两者最大区别就是用户计时器需要在用户程序中使用大量的用户界面基础设施,从而消耗更多的资源。而可等待计时器是内核对象,不仅可以在多线程间共享而且具备安全性 。用户计时器会产生WM_TIMER消息,这个消息被送到SetTimer设置的回调函数。此时只有一个线程得到通知。而可等待计时器对象可以被多个线程等待。 如果打算在计时器被触发时执行与用户界面相关的操作。使用用户计时器可使代码更容易编写。
信号量内核对象:与其他所有内核对象相同,它们也包含一个使用计数,但是它们还包括另外两个32bit值,一个最大资源计数和一个当前资源计数。最大资源计数表示信号量可以控制的最大资源数量,当前资源计数表示信号量当前可用资源的数量。
信号量的规则如下:
1.如果当前资源计数大于0,那么信号量处于触发状态
2.如果当前资源计数等于0,那么信号量处于未触发状态
3.系统绝对不会让当前资源计数变为负数
4.当前资源计数绝对不会大于最大资源计数
下面函数用来创建信号量内核对象:
HANDLE CreateSomaphore( PSECURITY_ATTRIBUTE psa, LONG lInitialCount, LONG lMaximuCount, PCTSTR pszName);lMaximumCount告诉系统应用程序能够处理的资源最大数量。 lInitialCount表示这些资源一开始有多少可供使用。
任何进程都可以调用OpenSemaphore来得到一个已经存在的信号量的句柄
HANDLE OpenSemaphore( DWORD dwDesiredAccess, BOOL hInheritHandle, PCTSTR pszName);线程通过调用ReleaseSemaphore来递增信号量的当前资源计数。
// 信号量对象句柄 HANDLE hSemaphore; UINT ThreadProc15(LPVOID pParam) { // 试图进入信号量关口 WaitForSingleObject(hSemaphore, INFINITE); // 线程任务处理 AfxMessageBox("线程一正在执行!"); // 释放信号量计数 ReleaseSemaphore(hSemaphore, 1, NULL); return 0; } UINT ThreadProc16(LPVOID pParam) { // 试图进入信号量关口 WaitForSingleObject(hSemaphore, INFINITE); // 线程任务处理 AfxMessageBox("线程二正在执行!"); // 释放信号量计数 ReleaseSemaphore(hSemaphore, 1, NULL); return 0; } UINT ThreadProc17(LPVOID pParam) { // 试图进入信号量关口 WaitForSingleObject(hSemaphore, INFINITE); // 线程任务处理 AfxMessageBox("线程三正在执行!"); // 释放信号量计数 ReleaseSemaphore(hSemaphore, 1, NULL); return 0; } …… void CSample08View::OnSemaphore() { // 创建信号量对象 hSemaphore = CreateSemaphore(NULL, 2, 2, NULL); // 开启线程 AfxBeginThread(ThreadProc15, NULL); AfxBeginThread(ThreadProc16, NULL); AfxBeginThread(ThreadProc17, NULL); }互斥量内核对象:用来确保一个线程独占对一个资源的访问,互斥量对象包含一个使用技术,线程ID以及一个递归计数。互斥量与关键段的行为完全相同。但是互斥量是内核对象,而关键段是用户模式下的同步对象(当对资源的竞争比较激烈的时候,这个时候将不得不进入内核模式)。
线程ID用来标识当前占用这互斥量的是系统中的哪个线程,递归计数表示这线程占用该互斥量的次数,互斥量可以确保保证正在访问内存块的任何线程会独占对内存块的访问权。
下面是互斥量的规则:
1.如果线程ID为0(无效线程ID),那么该互斥量不为任何线程所占用,它处于触发状态
2.如果线程ID为非0值,那么有一个线程已经占用了该互斥量,它处于未触发状态
3.与所有其他内核对象不同,操作系统对互斥量进行特殊处理,允许他们违反一些常规的规则。
要使用互斥量,进程必须先调用CreateMutexl来插件创建一个互斥量
HANDLE CreateMutex( PSECURITY_ATTRIBUTES psa, BOOL bInitialOwner, PCTSTR pszName); bInitialOwner用来控制互斥量的初始状态。如果传入false,那么表示互斥量不属于任何线程。线程ID和递归计数都为0。此时互斥量处于触发状态。如果为true,互斥量的线程ID将被设为主调线程的线程ID,递归计数被设为1。OpenMutex来打开一个已存在的互斥量。
HANDLE OpenMutex( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName);为了获得对被保护资源的访问权,线程要调用等待函数并传入互斥量句柄。在内部,等待函数会检查线程ID是否为0,如果为0,等待线程将互斥量对象线程ID设为当前线程ID,递归计数为1。否则,主调线程将会被挂起。当其他线程完成对保护资源的互斥访问,释放对互斥量的占有时,互斥量的线程ID被设为0,原来被挂起的线程变为可调度状态,并将互斥量对象对象ID设为此线程ID,递归计数为1。 前面一直提到递归计数,却没有解释它的意思。当线程试图等待一个未触发的互斥量对象,此时通常处于等待状态。但是系统会检查想要获得互斥量的线程的线程ID与互斥量对象内部记录的线程ID是否相同。如果相同,那么系统会让线程保持可调度状态,即使该互斥量尚未触发。每次线程等待成功一个互斥量,互斥对象的递归计数就会被设为1。因此,使递归对象大于1 的唯一途径是让线程多次等待同一个互斥量。
当目前占有互斥量的线程不再需要访问互斥资源时,它必须调用ReleaseMutex来释放互斥量。
BOOL ReleaseMutex(HANDLE hMutex);调用ReleaseMutex时该函数会检查调用线程ID是否与互斥量内部保存的线程ID相同。如果相同,那么递归计数会递减。否则函数执行失败返回false。如果线程成功等待了互斥量对象不止一次,那么线程必须调用相同次数的ReleaseMutex才能使对象的递归计数变成0。
WaitFotInputIdle函数:线程可以调用这函数来将自己挂起
DWORD WaitForInputIdle( HANDLE hProcess, DWORD dwMilliseconds );hProcess 进程的句柄。如果这个进程是一个控制台应用程序或者没有消息队列,函数立刻返回。 dwMilliseconds 超时间隔,单位为毫秒。如果 dwMilliseconds 设为 INFINITE,函数会一直等到进程空闲即初始化完成后才返回。
返回值 说明 0 线程初始化成功,等待结束。 WAIT_TIMEOUT 等待强制结束,线程初始化超出dwMilliseconds 。 WAIT_FAILED 出错。会设置GetLastError。
该函数可以使一个线程挂起,直到规定线程初始化完成,等待用户输入。这对于父进程和子进程之间的同步是极其有用的,因为CreateProcess函数不会等待新进程完成它的初始化工作。在试图与子线程通讯前,父线程可以使用WaitForInputIdle来判断子线程是否已经初始化完成。举例来说,在试图与新进程关联的窗口之前,进程应该先调用WaitForInputIdle。
BOOL StartProcess(char* strCmdLine) { BOOL bRet = FALSE; STARTUPINFO sinfo; PROCESS_INFORMATION pinfo; memset(&sinfo, 0, sizeof(sinfo)); sinfo.cb = sizeof(STARTUPINFO); char szDir[4]; szDir[0] = strCmdLine[0]; szDir[1] = strCmdLine[1]; szDir[2] = 0; if (CreateProcess(0, strCmdLine, 0, 0, 0, 0, 0, szDir, &sinfo, &pinfo)) { PushLog("进程已创建,等待初始化完成..."); if (WaitForInputIdle(pinfo.hProcess, 30000) == 0) { PushLog("进程创建完成,初始化完毕"); bRet = TRUE; } else { PushLog("%s在30秒内未初始化完成,或者有错误发生code=%d", strCmdLine, GetLastError()); } CloseHandle(pinfo.hThread); CloseHandle(pinfo.hProcess); } if (bRet == FALSE) { DWORD_G dwErr = GetLastError(); PushLog("%s在启动过程中有错误发生,code=%d", strCmdLine, dwErr); } return bRet; }线程也可以调用MsgWaitForMultipleObjects或MsgWaitForMultipleObjectEx,这使得线程等待需要自己处理的消息。
DWORD MsgWaitForMultipleObjects( DWORD nCount, // 表示pHandles所指的handles数组的元素个数,最大容量是MAXIMUM_WAIT_OBJECTS LPHANDLE pHandles, // 指向一个由对象handles组成的数组,这些handles的类型不需要相同 BOOL fWaitAll, // 是否等待所有的handles被激发才返回 DWORD dwMilliseconds, // 超时时间 DWORD dwWakeMask // 欲观察的用户输入消息类型 ); DWORD MsgWaitForMultipleObjectsEx( DWORD nCount, //句柄数组中句柄数目 LPHANDLE pHandles, //指向句柄数组的指针 DWORD dwMilliseconds, //以毫秒计的超时值 DWORD dwWakeMask, //要等待的输入事件类型 DWORD dwFlags //等待标志 );这些函数与waitformultipleobjetcs函数类似,不同之处在于,不仅内核对象被触发的时候调用线程也会变成可调度状态,而且当窗口消息需要被配送到一个由调用线程创建的窗口时,它们也会变成可调度状态。创建出口的线程和执行与用户界面相关的任务的线程不应该使用waitformutipleobjects而应该使用MsgWaitForMultipleObjectsEx。这是因为前者会妨碍线程对用户界面上的操作进行响应。
SignalObjectAndWait函数会通过一个原子操作来触发一个内核对象并等待另一个内核对象:
DWORD WINAPI SignalObjectAndWait( __in HANDLE hObjectToSignal, __in HANDLE hObjectToWaitOn, __in DWORD dwMilliseconds, __in BOOL bAlertable );hObjectToSignal 要通知的内核对象的句柄. hObjectToWaitOn 要等待的内核对像的句柄. dwMilliseconds 等待内核对象的时间,以毫秒为单位. bAlertable 如果这个值为TRUE,函数返回,并调用完成端口指定函数. 如果这个值为FALSE函数不返回,也不调用指定函数.
等你需要通知一个互斥内核对象并等待一个事件内核对象的时候,可以这么写:
ReleaseMutex(hMutex); WaitForSingleObject(hEvent, INFINITE);可是,这样的代码不是以原子的方式来操纵这两个内核对象。因此,可以更改如下: SignalObjectAndWait(hMutex, hEvent, INFINITE, FALSE);