Windows核心编程笔记(九)使用内核对象进行线程同步
2017-01-13 04:52
609 查看
上一章作者介绍了用户模式下的线程同步,虽然它们有很好的性能,但是他们功能有限并不是能够胜任实际工作中出现的复杂情况,以及特殊的要求,于是Windwos提供了一些用于线程同步的内核对象,它们根据不同的应用场景而设计,满足不同的环境下的线程同步的要求,因为是基于内核对象来完成同步,因此线程同步时可以跨进程的。
这些内核对象包括,事件,可等待计时器,信号量,互斥量。
在具体介绍每个线程同步内核对象之前,需要了解两个基础的用于等待内核对象的函数WaitForSingleObjectEx 和 WaitForMultipleObjects.
返回值
WAIT_ABANDONED (只在对象为互斥量类型时用到, 表示上一个线程因为错误,线程异常终止,内核对象才变为了有信号的)
WAIT_OBJECT_0 (内核对象有信号)
WAIT_TIMEOUT (等待时间到了)
WAIT_FAILED (参数错误,函数调用失败)
该API函数等待一个或者一组线程同步内核对象,由 bWaitAll指定等待方式, bWaitAll == 1在所有对象处于有信号状态返回, bWaitAll=0 只有其中一个处于有信号状态就返回
返回值
WAIT_OBJECT_0 to (WAIT_OBJECT_0 + nCount– 1) (WAIT_OBJECT_0+N)表示第N个对象处于有信号状态在bWaitAll为1时,如果bWaitAll为0 WAIT_OBJECT_0表示所有对象处于有信号状态,成功返回。
WAIT_ABANDONED_0 to (WAIT_ABANDONED_0 + nCount– 1) (如果bWaitAll为1,WAIT_ABANDONED_0表示句柄数组中至少有一个是互斥量对象的句柄,如果bWaitAll为0,那么WAIT_ABANDONED_0+N表示句柄数组中第N个是一个互斥量句柄,上一个线程没有释如果放互斥量的情况下异常终止)
WAIT_TIMEOUT (同WaitForSingleObject)
WAIT_FAILED (WaitForSingleObject)
(一)事件内核对象
事件内核对象在上一章的用户模式的同步对象 CriticalSection 中已经提到,CriticalSection只是用了它的自动重置模式,自动重置模式在WaitForSingleObject获取到有信号后自动将对象设置为无信号状态并返回。
临界区对象在EnterCriticalSection中调用执WaitForSingleObjec行等待,在LeaveCriticalSection中调用SetEvent将事件对象重置为有信号状态。
此外事件内核对象可以使用手动重置模式,在手动模式下,WaitForSingleObject并改变其状态,需要手动调用SetEvent将对象置为有信号状态,或者调用ResetEvent将对象置为无信号状态,在将对象置为有信号状态时,所有使用WaitXX执行等待的线程都可以返回WAIT_OBJECT_0 得到调度。
API:
适应:在自动模式下与 临界区对象 互斥量 SRWLock的互斥模式 功能是一样的,都保证对共享资源的访问在同一时间只运行一个线程独占。
区别 :临界区对象在使用内核机制前有个一个旋转锁等待循环,如果在确定对共享资源独占期间占用很少的CPU周期,应该使用临界区对象。
如果区分写共享资源和读共享资源的线程,即存在生产和消费两种类型的线程,使用SRWLock
如果要进行进程间的同步,或者在独占共享资源之后的操作可能占用很多的CPU周期,如等待用户输入并把用户的输入写入共享资源,应该使用事件或者互斥量,这两个内核同步对象,使线程在无信号状态不参与调度,不占用CPU,而不是SRWLock那样循环的使用原子访问和NOP
互斥量和事件对象的区别很小,互斥量允许一个在异常终止后系统自己是否它对对象的占用,从而避免死锁。事件对象则多了一个手动模式,允许多个线程同时得到执行
(二)可等待的计时器
可等待计时器,类似于SetTimer,它设置一个定时器,并可以设置周期循环触发的时间,在时间到期时所有等待线程可以得到执行(手动模式),或者一个等待线程被唤醒(自动模式)。同时它还可以设置一个APC回调函数,在时间到期时将ACP挂入 SetWaitableTimer 主调线程的APC队列中。
API:
取消对象的活动状态,不再触发。
测试代码 使用自动重置模式,10秒后触发,之后每秒触发一次
运行结果
hTimer = CreateWaitableTimer(NULL, FALSE, NULL); 第二个参数改为TRUE 手动模式后
每个线程一直都处于可调度状态
可以等待计数器内核对象的适用情况非常的明确,当需要一个定时器在某和时间点唤醒等待线程,或者按一定时间循环唤醒时。
(三)信号量内核对象
信号量内核对象用来对资源进行计数,在需要对资源使用做限制的时候信号量非常有用,在内部它存放了一个当前可用资源数的变量,和一个最大资源数的变量,每个等待线程使用等待函数时,'如果可以资源数大于0,将其值减一并返回,如果可以资源数已经为0则等待函数进入睡眠,可以通过ReleaseSemaphore增加可以用资源数量,对计数访问修改都是使用的原子操作,保证了计数在多线程下不被破坏。
API:
测试代码:
运行结果
(四)互斥量对象
互斥量内核对象用来确保一个线程独占对一个资源的访问。互斥量对象包含一个使用计数,线程ID,和一个递归计数。互斥量与关键段的行为完全相同,但互斥量是内核对象,而关键段是用户模式下的同步对象。
互斥量规则:
(1)如果线程ID为0(无效线程ID),那么该互斥量不为任何线程占用,它处于触发在状态
(2)如果线程ID为非零值,那么有一个线程已经占用了该互斥量,它处于未触发在状态
(3)与其他内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规的规则。
为了获得对被保护资源的访问权,线程要调用等待函数并传入互斥量句柄。在内部,等待函数会检查线程ID是否为0,如果为0,等待线程将互斥量对象线程ID设为当前线程
ID,递归计数为1。否则,主调线程将会被挂起。当其他线程完成对保护资源的互斥访问,释放对互斥量的占有时,互斥量的线程ID被设为0,原来被挂起的线程变为可调度状态,并将互斥量对象对象ID设为此线程ID,递归计数为1。
前面一直提到递归计数,却没有解释它的意思。当线程试图等待一个未触发的互斥量对象,此时通常处于等待状态。但是系统会检查想要获得互斥量的线程的线程ID与互斥量对象内部记录的线程ID是否相同。如果相同,那么系统会让线程保持可调度状态,即使该互斥量尚未触发。每次线程等待成功一个互斥量,互斥对象的递归计数就会被设为1。因此,使递归对象大于1 的唯一途径是让线程多次等待同一个互斥量。
当目前占有互斥量的线程不再需要访问互斥资源时,它必须调用ReleaseMutex来释放互斥量。
API:
(五)各种同步对象的概括
每种同步内核对象都有个OpenXX版本的函数,用来打开已经创建过的对象,如OpenMutex,OpenEvent,
每种创建同步内核对象的函数都有一个对应的后缀为Ex,的版本 如CreateSemaphoreEx,CreateEventEx, 带后缀的版本多了一个__in DWORD dwDesiredAccess参数,用来设置创建的对象可以用什么访问权限来使用。
每种同步内核对象都是可以具名的,所有的内核对象名字同在一个命名空间内,如果要创建的对象名字已存在,创建函数失败,GetLastError 返回ERROR_ALREADY_EXISTS
每种同步内核对象在不使用的时候都要调用CloseHandle关闭,不然会造成内核对象泄露。
每种同步内核对象都可以拥有不同进程间的线程同步,可以使用DuplicationHandle,句柄继承,打开同名对象的方式,让两个进程中各自的线程引用同一个内核对象来完成同步工作。
(六)其他等待函数
线程在等待内核对象变为有信号的同时,如果有窗口消息到来,线程也可以变为可调度状态。
被调试进程有调试事件时触发,否则一直等待
将一个对象置为有信号的同时,等待另一个对象,原子操作在内部。
这些内核对象包括,事件,可等待计时器,信号量,互斥量。
在具体介绍每个线程同步内核对象之前,需要了解两个基础的用于等待内核对象的函数WaitForSingleObjectEx 和 WaitForMultipleObjects.
DWORD WINAPI WaitForSingleObject( __in HANDLE hHandle, //内核对象句柄 __in DWORD dwMilliseconds //等待的时间、单位:毫秒 如果指定(INFINITE)表示无信号一直等待。 );
返回值
WAIT_ABANDONED (只在对象为互斥量类型时用到, 表示上一个线程因为错误,线程异常终止,内核对象才变为了有信号的)
WAIT_OBJECT_0 (内核对象有信号)
WAIT_TIMEOUT (等待时间到了)
WAIT_FAILED (参数错误,函数调用失败)
DWORD WINAPI WaitForMultipleObjects( __in DWORD nCount, (多少个要等待的对象) __in const HANDLE *lpHandles, 内核对象句柄数组 __in BOOL bWaitAll, (是否等待所有对象有信号才返回) __in DWORD dwMilliseconds (等待时间) );
该API函数等待一个或者一组线程同步内核对象,由 bWaitAll指定等待方式, bWaitAll == 1在所有对象处于有信号状态返回, bWaitAll=0 只有其中一个处于有信号状态就返回
返回值
WAIT_OBJECT_0 to (WAIT_OBJECT_0 + nCount– 1) (WAIT_OBJECT_0+N)表示第N个对象处于有信号状态在bWaitAll为1时,如果bWaitAll为0 WAIT_OBJECT_0表示所有对象处于有信号状态,成功返回。
WAIT_ABANDONED_0 to (WAIT_ABANDONED_0 + nCount– 1) (如果bWaitAll为1,WAIT_ABANDONED_0表示句柄数组中至少有一个是互斥量对象的句柄,如果bWaitAll为0,那么WAIT_ABANDONED_0+N表示句柄数组中第N个是一个互斥量句柄,上一个线程没有释如果放互斥量的情况下异常终止)
WAIT_TIMEOUT (同WaitForSingleObject)
WAIT_FAILED (WaitForSingleObject)
(一)事件内核对象
事件内核对象在上一章的用户模式的同步对象 CriticalSection 中已经提到,CriticalSection只是用了它的自动重置模式,自动重置模式在WaitForSingleObject获取到有信号后自动将对象设置为无信号状态并返回。
临界区对象在EnterCriticalSection中调用执WaitForSingleObjec行等待,在LeaveCriticalSection中调用SetEvent将事件对象重置为有信号状态。
此外事件内核对象可以使用手动重置模式,在手动模式下,WaitForSingleObject并改变其状态,需要手动调用SetEvent将对象置为有信号状态,或者调用ResetEvent将对象置为无信号状态,在将对象置为有信号状态时,所有使用WaitXX执行等待的线程都可以返回WAIT_OBJECT_0 得到调度。
API:
HANDLE WINAPI CreateEvent( __in_opt LPSECURITY_ATTRIBUTES lpEventAttributes, __in BOOL bManualReset, //TRUE自动模式 FALSE手动模式 __in BOOL bInitialState,//初始值是否是有信号的,TRUE (是),FALSE(不是) __in_opt LPCTSTR lpName );
BOOL WINAPI SetEvent( __in HANDLE hEvent );
BOOL WINAPI ResetEvent( __in HANDLE hEvent );
适应:在自动模式下与 临界区对象 互斥量 SRWLock的互斥模式 功能是一样的,都保证对共享资源的访问在同一时间只运行一个线程独占。
区别 :临界区对象在使用内核机制前有个一个旋转锁等待循环,如果在确定对共享资源独占期间占用很少的CPU周期,应该使用临界区对象。
如果区分写共享资源和读共享资源的线程,即存在生产和消费两种类型的线程,使用SRWLock
如果要进行进程间的同步,或者在独占共享资源之后的操作可能占用很多的CPU周期,如等待用户输入并把用户的输入写入共享资源,应该使用事件或者互斥量,这两个内核同步对象,使线程在无信号状态不参与调度,不占用CPU,而不是SRWLock那样循环的使用原子访问和NOP
互斥量和事件对象的区别很小,互斥量允许一个在异常终止后系统自己是否它对对象的占用,从而避免死锁。事件对象则多了一个手动模式,允许多个线程同时得到执行
(二)可等待的计时器
可等待计时器,类似于SetTimer,它设置一个定时器,并可以设置周期循环触发的时间,在时间到期时所有等待线程可以得到执行(手动模式),或者一个等待线程被唤醒(自动模式)。同时它还可以设置一个APC回调函数,在时间到期时将ACP挂入 SetWaitableTimer 主调线程的APC队列中。
API:
HANDLE WINAPI CreateWaitableTimer( __in_opt LPSECURITY_ATTRIBUTES lpTimerAttributes, __in BOOL bManualReset, //自动还是手动模式 __in_opt LPCTSTR lpTimerName //内核对象名字 );
BOOL WINAPI SetWaitableTimer( __in HANDLE hTimer, __in const LARGE_INTEGER *pDueTime, //触发的时间,如果为负数表示相对时间,单位纳秒 __in LONG lPeriod, //触发后多久循环触发一次一次 __in_opt PTIMERAPCROUTINE pfnCompletionRoutine, //APC回调函数地址 __in_opt LPVOID lpArgToCompletionRoutine, //回调函数参数 __in BOOL fResume //是否唤醒睡眠中的系统,利用电源管理 );
取消对象的活动状态,不再触发。
BOOL WINAPI CancelWaitableTimer( __in HANDLE hTimer );
测试代码 使用自动重置模式,10秒后触发,之后每秒触发一次
#include <iostream> #include <stdio.h> #include <windows.h> #include <thread> #include <winbase.h> //using namespace std; using std::cout; using std::endl; HANDLE hTimer = NULL; volatile LONG g_IsPassed = 0; void ThreadForTimer(int pParameter) { //int ID = *static_cast<int*>(pParameter); int ID = pParameter; InterlockedAdd(&g_IsPassed,1); while(1) { cout.flush(); if(WaitForSingleObject(hTimer,INFINITE) == WAIT_OBJECT_0) { cout<<"Thread "<<ID<<" has obtained the single Time:"<<GetTickCount()<<endl; } else { cout<<"Thread exception occurred"<<endl; } } } int main(int argc, char *argv[]) { LARGE_INTEGER liDueTime; liDueTime.QuadPart = -100000000L; // Create an unnamed waitable timer. hTimer = CreateWaitableTimer(NULL, FALSE, NULL); if (NULL == hTimer) { printf("CreateWaitableTimer failed (%d)\n", GetLastError()); return 1; } printf("Waiting for 10 seconds Time:%d \n ",GetTickCount()); if (!SetWaitableTimer(hTimer, &liDueTime, 1000, NULL, NULL, 0)) { printf("SetWaitableTimer failed (%d)\n", GetLastError()); return 2; } for(int i = 0; i != 5; ++i) { new std::thread(ThreadForTimer,i); while(1) { if(InterlockedCompareExchange(&g_IsPassed,0,1) == 1) break; YieldProcessor(); } } SleepEx(INFINITE,TRUE); return 0; }
运行结果
hTimer = CreateWaitableTimer(NULL, FALSE, NULL); 第二个参数改为TRUE 手动模式后
每个线程一直都处于可调度状态
可以等待计数器内核对象的适用情况非常的明确,当需要一个定时器在某和时间点唤醒等待线程,或者按一定时间循环唤醒时。
(三)信号量内核对象
信号量内核对象用来对资源进行计数,在需要对资源使用做限制的时候信号量非常有用,在内部它存放了一个当前可用资源数的变量,和一个最大资源数的变量,每个等待线程使用等待函数时,'如果可以资源数大于0,将其值减一并返回,如果可以资源数已经为0则等待函数进入睡眠,可以通过ReleaseSemaphore增加可以用资源数量,对计数访问修改都是使用的原子操作,保证了计数在多线程下不被破坏。
API:
HANDLE WINAPI CreateSemaphore( __in_opt LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, __in LONG lInitialCount, 可用数量 __in LONG lMaximumCount, 最大数量 __in_opt LPCTSTR lpName );
BOOL WINAPI ReleaseSemaphore( __in HANDLE hSemaphore, __in LONG lReleaseCount, 增加多少
__out_opt LPLONG lpPreviousCount );
测试代码:
#include <iostream> #include <stdio.h> #include <windows.h> #include <thread> #include <winbase.h> #include <string> #include <random> #include <vector> //using namespace std; using std::cout; using std::endl; HANDLE g_hSemaphore; HANDLE g_hEvent; std::vector<int> vtrBuffer; void ThreadForSemaphoreInc() { std::random_device rd; while(1) { if(WaitForSingleObject(g_hEvent,INFINITE) == WAIT_OBJECT_0) { for(int i = 0; i !=10; ++i) { vtrBuffer.push_back(rd()%100); } ReleaseSemaphore(g_hSemaphore,10,NULL); SetEvent(g_hEvent); } cout<<"The buffer has been full"<<endl<<endl; Sleep(5*1000); } } void ThreadForSemaphoreDec() { HANDLE handles[2] = {g_hEvent,g_hSemaphore}; while(1) { if(WaitForMultipleObjects(2,handles,TRUE,INFINITE) == WAIT_OBJECT_0) { cout<<"Decreasing the buffer,the popuped value is "<<vtrBuffer.back()<<endl; vtrBuffer.pop_back(); SetEvent(g_hEvent); } } } int main(int argc, char *argv[]) { g_hEvent = CreateEvent(NULL,FALSE,TRUE,NULL); if(!g_hEvent) cout<<"CreateEvent failed and the error code is "<<GetLastError()<<endl; g_hSemaphore = CreateSemaphore(NULL,0,10,NULL); if(!g_hEvent) cout<<"CreateSemaphore failed and the error code is "<<GetLastError()<<endl; new std::thread(ThreadForSemaphoreInc); new std::thread(ThreadForSemaphoreDec); Sleep(1000*1000); return 0; }
运行结果
(四)互斥量对象
互斥量内核对象用来确保一个线程独占对一个资源的访问。互斥量对象包含一个使用计数,线程ID,和一个递归计数。互斥量与关键段的行为完全相同,但互斥量是内核对象,而关键段是用户模式下的同步对象。
互斥量规则:
(1)如果线程ID为0(无效线程ID),那么该互斥量不为任何线程占用,它处于触发在状态
(2)如果线程ID为非零值,那么有一个线程已经占用了该互斥量,它处于未触发在状态
(3)与其他内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规的规则。
为了获得对被保护资源的访问权,线程要调用等待函数并传入互斥量句柄。在内部,等待函数会检查线程ID是否为0,如果为0,等待线程将互斥量对象线程ID设为当前线程
ID,递归计数为1。否则,主调线程将会被挂起。当其他线程完成对保护资源的互斥访问,释放对互斥量的占有时,互斥量的线程ID被设为0,原来被挂起的线程变为可调度状态,并将互斥量对象对象ID设为此线程ID,递归计数为1。
前面一直提到递归计数,却没有解释它的意思。当线程试图等待一个未触发的互斥量对象,此时通常处于等待状态。但是系统会检查想要获得互斥量的线程的线程ID与互斥量对象内部记录的线程ID是否相同。如果相同,那么系统会让线程保持可调度状态,即使该互斥量尚未触发。每次线程等待成功一个互斥量,互斥对象的递归计数就会被设为1。因此,使递归对象大于1 的唯一途径是让线程多次等待同一个互斥量。
当目前占有互斥量的线程不再需要访问互斥资源时,它必须调用ReleaseMutex来释放互斥量。
API:
HANDLE WINAPI CreateMutex( __in_opt LPSECURITY_ATTRIBUTES lpMutexAttributes, __in BOOL bInitialOwner, __in_opt LPCTSTR lpName );
BOOL WINAPI ReleaseMutex( __in HANDLE hMutex );
(五)各种同步对象的概括
每种同步内核对象都有个OpenXX版本的函数,用来打开已经创建过的对象,如OpenMutex,OpenEvent,
每种创建同步内核对象的函数都有一个对应的后缀为Ex,的版本 如CreateSemaphoreEx,CreateEventEx, 带后缀的版本多了一个__in DWORD dwDesiredAccess参数,用来设置创建的对象可以用什么访问权限来使用。
每种同步内核对象都是可以具名的,所有的内核对象名字同在一个命名空间内,如果要创建的对象名字已存在,创建函数失败,GetLastError 返回ERROR_ALREADY_EXISTS
每种同步内核对象在不使用的时候都要调用CloseHandle关闭,不然会造成内核对象泄露。
每种同步内核对象都可以拥有不同进程间的线程同步,可以使用DuplicationHandle,句柄继承,打开同名对象的方式,让两个进程中各自的线程引用同一个内核对象来完成同步工作。
(六)其他等待函数
DWORD WaitForInputIdle( HANDLE hProcess, // handle to process DWORD dwMilliseconds // time-out interval );用于等待一个进程,第一次创建窗口。
DWORD WINAPI MsgWaitForMultipleObjects( __in DWORD nCount, __in const HANDLE *pHandles, __in BOOL bWaitAll, __in DWORD dwMilliseconds, __in DWORD dwWakeMask );
线程在等待内核对象变为有信号的同时,如果有窗口消息到来,线程也可以变为可调度状态。
BOOL WaitForDebugEvent( LPDEBUG_EVENT lpDebugEvent, // debug event information DWORD dwMilliseconds // time-out value );
被调试进程有调试事件时触发,否则一直等待
DWORD WINAPI SignalObjectAndWait( __in HANDLE hObjectToSignal, __in HANDLE hObjectToWaitOn, __in DWORD dwMilliseconds, __in BOOL bAlertable );
将一个对象置为有信号的同时,等待另一个对象,原子操作在内部。
相关文章推荐
- 《windows核心编程学习笔记》——使用互斥量变量内核对象进行线程同步
- 第九章 使用内核对象进行线程同步
- 进程与线程(五)用内核对象进行线程同步(上)
- 进程与线程(六)用内核对象进行线程同步(下)
- (摘自windows核心编程之用内核对象进行线程同步)
- 第9章 用内核对象进行线程同步(1)_事件对象(Event)
- 《Windows核心编程》——九 用内核对象进行线程同步
- Windows核心编程学习九:利用内核对象进行线程同步
- 摘自windows核心编程之用内核对象进行线程同步
- 用内核对象进行线程同步
- 《Windows核心编程系列》八谈谈用内核对象进行线程同步
- 第九章:用内核对象进行线程同步(二) .
- Windows-核心编程-09-如何用内核对象进行线程同步-信号内核对象
- 《Windows核心编程 5th》读书笔记----第9章 用内核对象进行线程同步
- windows 用内核对象进行线程同步
- (摘自windows核心编程之用内核对象进行线程同步)
- 线程同步的内核对象的使用
- Windows核心编程学习九:利用内核对象进行线程同步
- Windows核心编程 用内核对象进行线程同步