线程同步——内核对象(互斥、事件、信号量、可等待计时器)
来源:互联网 发布:单身狗的嘲讽知乎 编辑:程序博客网 时间:2024/06/06 01:17
三、内核模式下的线程同步
- Windows系统中有多种机制可用于线程同步,它们一般都被称之为内核对象(并非全部),一般我们常用的有以下几种:
- 互斥对象(Mutex)
- 事件对象(Event)
- 信号量(Semaphore)
- 可等待计时器(Waitable Timer)
0.等待函数
- WaitForSingleObject
等待函数的作用是使一个线程进入到等待状态,直到指定的内核对象被触发为止,其函数原型如下所示:
DWORD WaitForSingleObject( _In_ HANDLE hHandle, //内核对象句柄 _In_ DWORD dwMiliseconds //等待超时时间(微秒,INFI));/*********returnWAIT_OBJECT_0: 成功返回WAIT_TIMEOUT: 超时返回WAIT_FAILED: 传入的参数错误*/
在创建线程时使用等待函数后,此函数会在等待超时或线程结束时返回,因此我们的主线程一次只能启动一个线程,从而避免上述例子中多线程访问同一个数据时所引发的问题.
- WaitForMultipleObjects
与WaitForSingleObject类似,唯一的不同之处在于它允许调用线程同时检查多个内核对象的触发状态
DWORD WaitForMultipleObjects( DWORD dwCount, // 检查的内核对象的数量 CONST HANDLE* phObjects, // 内核对象句柄数组 BOOL bWaitAll, // 是否在所有内核对象触发之后返回 DWORD dwMilliseconds) // 等待时间/************ returnWAIT_FAILEDWAIT_TIMEOUT// 如果 bWaitAll 是 TRUE ,则返回 WAIT_OBJECT_0// 如果 bWaitAll 是 FALSE, 则返回值是 WAIT_OBJECT_0和(WAIT_OBJECT_0 + dwCount - 1) 之间的任意一个值,内核对象数组的下标为:返回值 - WAIT_OBJECT_0*/
对一些内核对象来说,成功地调用 WaitForSingleObject 与 WaitForMultipleObjects 事实上会改变对象的状态。如:自动重置事件内核对象,当时间对象被触发的时候,函数会检测到这一情况,这时它可以直接返回 WAIT_OBJECT_0 给调用线程。但是,就在函数返回之前,它会使事件变为非触发状态——这就是等待成功所引起的副作用。 WaitForMultipleObjects是以原子方式工作的,当函数检查内核对象的状态时,任何其他线程都不能在背后修改对象的状态。
1.事件对象
事件(Event) 是在线程同步中最常使用的一种同步对象,而且比其他对象要简单一些。像其他对象一样,事件包含了一个使用计数,一个是用来标识自动重置/手动重置的BOOL值,另一个是表示事件有没有触发的BOOL值
- 事件对象有两种状态,他们分别是:
- 手动状态:被触发后所有等待该事件的线程都将变为可调度状态,常用于控制具有较强自定义要求的多线程同步环境.
- 自动状态:被触发后只有一个等待事件线程会变成可调度状态。
/*关键函数*/HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes,//属性 BOOL hManualReset, //手工重置 BOOL bInitialState, //初始状态 LPCTSTR lpName //事件对象名称);//注:非手工状态下,调用SetEvent放行一个线程后,会自动再次设为无//信号状态,直到再次调用SetEvent/*设置标记为有信号状态(释放等待函数)*/BOOL SetEvent( HANDLE hEvent //事件对象句柄);/*重置标记为无信号状态(阻塞等待函数)*/BOOL WINAPI ResetEvent( HANDLE hEvent //事件对象句柄);//打开内核对象HANDLE OpenEvent( DWORD dwDesiredAccess,//对事件对象的请求访问权限 BOOL bInheritHandle,//是否能继承 LPCTSTR lpName //事件对象的名字);
一个防多开的例子
if (!OpenEvent(EVENT_MODIFY_STATE, TRUE,L"Global\\Text")) CreateEvent(NULL, TRUE, TRUE, L"Global\\Text");else return 0;
示例:
int g_nNum = 0;HANDLE g_hEventA = nullptr;HANDLE g_hEventB = nullptr;DWORD WINAPI ThreadProcA(LPVOID lpParam) { for (int i = 0; i < 5; i++){ WaitForSingleObject(g_hEventA, INFINITE); ResetEvent(g_hEventB); printf("%d ", g_nNum++); SetEvent(g_hEventB); } return 0;}DWORD WINAPI ThreadProcB(LPVOID lpParam){ for (int i = 0; i < 5; i++){ WaitForSingleObject(g_hEventB, INFINITE); ResetEvent(g_hEventA); printf("%d ", g_nNum++); SetEvent(g_hEventA); } return 0;}int _tmain(int argc, _TCHAR* argv[]){ if (!(g_hEventA = CreateEvent(NULL, TRUE, TRUE, NULL))) return 0; if (!(g_hEventB = CreateEvent(NULL, TRUE, FALSE, NULL))) return 0; CreateThread(NULL, 0, ThreadProcA, NULL, 0, nullptr); CreateThread(NULL, 0, ThreadProcB, NULL, 0, nullptr); system("pause"); return 0;}
2. 可等待的计时器内核对象(Waitable Timer)
可等待的计时器(Waitable Timer): 会在某个指定的时间触发,或每隔一段时间触发一次。
创建可等待的计时器:
HANDLE CreateWaitableTimer( PSECURITY_ATTRIBUTES psa, BOOL bManualReset, PCTSTR pszName);
打开获取一个已经存在的可等待计时器的句柄:
HANDLE OpenWaitableTimer( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName);
在创建可等待的计时器的时候,计时器总是处于未触发状态。等我们想要触发计时器的时候必须调用 SetWaitableTimer 函数:
BOOL SetWaitableTimer( HANDLE hTimer, const LARGE_INTEGER *pDueTime, // 表示计时器第一次触发的时间 LONG lPeriod, // 在第一次触发之后,多长时间触发一次 PTIMERAPCROUTINE pfnCompletionRoutine, // 计时器函数 PVOID pvArgToCompletionRoutine, // 传入参数 BOOL bResume); // 是否支持挂起与恢复
取消计时器:
BOOL CancelWaitableTimer(HANDLE hTimer); //取消一个计时器
//第一次触发时间为2008年1月1日下午1:00,之后每隔6小时触发一次HANDLE hTimer;SYSTEMTIME st;FILETIME ftLocal, ftUTC;LARGE_INTEGER liUTC;hTimer = CreateWaitableTimer(NULL, FALSE, NULL);st.wYear = 2008;st.wMonth = 1;st.wDayOfWeek = 0; //忽略st.wDay = 1;st.wHour = 13;st.wMinute = 0;st.wSecond = 0;st.wMilliseconds = 0;SystemTimeToFileTime(&st, &ftLocal);// 将本地时间转换为 UTC 时间LocalFileTimeToFileTime(&ftLocal, &ftUTC);// 将 FILETIME 转换为 LARGE_INTEGER , FILETIME 与 LARGE_INTEGER 二进制格式一致,但是前者是地址是32为边界,后者是64位边界liUTC.LowPart = ftUTC.dwLowDateTime;liUTC.HighPart = ftUTC.dwHighDateTime;SetWaitableTimer(hTimer, &liUTC, 6*60*60*1000, NULL, NULL, FALSE);// 触发一次就不再触发的计时器,即给 lPeriod参数传入0就可以了。
创建APC(asynchronous procedure call, 异步过程调用)计时器
// 异步过程调用原型VOID APIENTRY TimerAPCRoutine(PVOID pvArgToCompletionRoutine, DWORD dwTimerLowValue, DWORD dwTimerHighValue){ FILETIME ftUTC, ftLocal; SYSTEMTIME st; TCHAR szBuf[256]; ftUTC.dwLowDateTime = dwTimerLowValue; ftUTC.dwHighDateTime = dwTimerHighValue; FileTimeToLocalFileTime(&ftUTC, &ftLocal); FileTimeToSystemTime(&ftLocal, &st); GetDateFormat(LOCALE_USER_DEFAULT, DATE_LONGDATE, &st, NULL, szBuf, _countof(szBuf)); _tcscat_s(szBuf, _countof(szBuf), TEXT(" ")); GetTimeFormat(LOCALE_USER_DEFAULT, 0, &st, NULL, _tcschr(szBuf, TEXT('\0')), (int)(_countof(szBuf) - _tcslen(szBuf))); //Show the time to the user MessageBox(NULL, szBuf, TEXT("Timer went off at..."), MB_OK);}void SomeFunc() { // Create a timer. (It doesn't matter whether it's manual-reset // or auto-reset.) HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL); LARGE_INTEGER li = {0}; SetWaitableTimer(hTimer, &li, 5000, TimerAPCRoutine, NULL, FALSE); SleepEx(INFINITE, TRUE); CloseHandle(hTimeer);}
SetWaitableTimer线程必须是由于调用 SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx、MsgWaitForMultipleObjectsEx 或 SignalObjectAndWait 而进入等待状态,APC异步调用才会被调用。
3.信号量
- 信号量是一种用于管理多个线程的复杂同步问题的解决方案,它可以限制某一时间段最多有多少个线程可以同时处于运行状态
- 信号量实现这个功能的原理是维护了一个计数器,计数器的值可以在0至用户指定的最大值之间,当一个线程完成了对信号量的等待后,计数器的值增加,当一个信号量被释放时,信号量的计数器减少。
- 当计数器的值为0时,信号量处于无信号状态(阻塞),不为0时则信号量处于有信号状态(释放)
- 如果我们将信号量的最大值设为1,那么它的作用与互斥对象将完全一致
信号量关键函数:
/*创建信号量*/HANDLE WINAPI CreateSemaphore( _In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,//属性 _In_ LONG lInitialCount, //信号初始值 _In_ LONG lMaximumCount,//信号最大值 _In_opt_ LPCTSTR lpName //信号量名称 );/*释放信号量*/BOOL ReleaseSemaphore( HANDLE hSemaphore, //信号量句柄 LONG lReleaseCount, //释放的信号量数量 LPLONG lpPreviousCount //返回信号量上次值);/*打开信号量*/HANDLE WINAPI OpenSemaphore( DWORD dwDesiredAccess, //对信号量的请求访问权限 BOOL bInheritHandle, //是否允许子进程继承此句柄 LPCTSTR lpName //信号量名称);
示例
int g_nNum = 0;HANDLE g_hSemaphore = nullptr;DWORD WINAPI ThreadProc(LPVOID lpParam){ for (int i = 0; i < 5; i++) { WaitForSingleObject(g_hSemaphore, INFINITE); printf("%d", g_nNum++); ReleaseSemaphore(g_hSemaphore, 1, NULL); } return 0;}int _tmain(int argc, _TCHAR* argv[]){ if (!(g_hSemaphore=CreateSemaphore(NULL,0,1,NULL)) ) { return 0; } CreateThread(NULL, 0, ThreadProc, NULL, 0, nullptr); CreateThread(NULL, 0, ThreadProc, NULL, 0, nullptr); system("pause"); return 0;}
4.互斥对象
互斥对象是一个非常简单的多线程同步内核对象,如果一个信号量未被线程所拥有(被等待函数获取),那么它是”有信号状态(非阻塞)”,只要它被线程获取,那么它就会变成”无信号状态(阻塞)”,需要注意的是,单一互斥对象只对同一线程有效,以下是互斥对象的一些常用API
//创建互斥对象HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, //属性 BOOL bInitialOwner, //初始状态 LPCTSTR lpName //互斥对象名称);//释放互斥对象BOOL ReleaseMutex(HANDLE hMutex); //互斥对象句柄//打开互斥对象HANDLE OpenMutex( DWORD dwDesiredAccess, //对互斥对象的请求访问权限 BOOL bInheritHandle,//是否希望子进程能够继承句柄 LPCTSTR lpName //互斥对象名称);
如果线程成功地等待了互斥量对象不止一次,那么线程必须调用 ReleaseMutex 相同的次数才能使对象递归计数变成0.当递归计数变成0的时候,函数还会将线程ID设为0,这样就触发了对象。
互斥对象—示例
int g_nNum = 0;HANDLE g_hMutex = nullptr;DWORD WINAPI ThreadProc(LPVOID lpParam){ for (int i = 0; i < 5; i++) { WaitForSingleObject(g_hMutex,INFINITE); printf("%d",g_nNum++); ReleaseMutex(g_hMutex); } return 0;}int _tmain(int argc, _TCHAR* argv[]){ if (!(g_hMutex=CreateMutex(NULL, FALSE,NULL))) return 0; CreateThread(NULL, 0, ThreadProc, NULL, 0, nullptr); CreateThread(NULL, 0, ThreadProc,NULL, 0, nullptr); system("pause"); return 0;}
互斥与临界区的比较:
总结
线程同步对象速查:
其他的线程同步函数
- 异步设备I/O
异步设备IO(asynchronous device I/O)允许线程开始读取操作或写入操作,但不必等待读取操作或写入操作完成。设备对象是可同步的内核对象。 - WaitForInputIdle函数(将自己挂起)
DWORD WaitForInputIdle(
HANDLE hProcess,
DWORD dwMilliseconds); - MsgWaitForMultipleObjects(Ex)既可以等待内核触发状态,也可以处理消息
- WaitForDebugEvent 函数等待调试消息
- SignalObjectAndWait 函数 通过一个原子操作触发一个内核对象并等待另一个内核对象
调用这个函数时,参数hObjectToSignal标识的必须是一个互斥量、信号量或事件。任何其他类型的对象将导致函数返回 WAIT_FAILED,这时调用GetLastError会返回ERROR_INVALID_HANDLE。该函数内部会检查对象的类型并分别执行与ReleaseMutex、ReleaseSemaphore、SetEvent 等价的操作。 - 使用等待链遍历API检测死锁
- 线程同步——内核对象(互斥、事件、信号量、可等待计时器)
- Chapter09-“内核模式下的线程同步”之可等待的计时器内核对象
- 白话windows多线程同步之可等待计时器内核对象
- 【Windows】线程漫谈——线程同步之等待函数和事件内核对象
- 可等待的计时器内核对象
- 内核对象--可等待计时器WaitableTimer(一)
- 内核对象--可等待计时器WaitableTimer(二)
- 多线程 -- 可等待的计时器内核对象
- 四种进程或线程同步互斥的方法:临界区、互斥对象、事件对象、信号量
- 进程、线程同步互斥学习 —— 信号量
- 线程同步与互斥——信号量
- -【内核对象线程同步】互斥对象内核对象
- 线程同步--信号量内核对象
- 多线程同步方法:临界区、事件、信号量、互斥对象
- 线程同步互斥之信号量对象(Semaphore)
- C++ 临界区、互斥对象、信号量、内核事件
- 用内核对象进行线程同步——信号量内核对象
- 可等待计时器内核对象的使用(CreateWaitableTimer)
- 资讯精选 | 云战略下的安全思维转型与新认知
- AngularJS 表单基本的验证功能
- Linux下Nginx安装
- leetcode 341. Flatten Nested List Iterator
- Mysql 支持存储表情
- 线程同步——内核对象(互斥、事件、信号量、可等待计时器)
- bzoj 4034 [HAOI2015]树上操作
- vmware上用kali破解wifi
- codeforces 834 C The Meaningless Game
- 小米5X 手机格式化照片原来这么简单就能找回,你知道吗
- 链表笔记
- PHP 计算两时间的时间差,倒计时
- Maximum Sequence
- PHPStorm 使用正则批量查询替换并自动转换大小写的方法