遗弃问题
互斥量概述
- 互斥量与所有其他内核对象有所不同,这是因为它们具有“线程所有权”的概念。
换句话说,互斥量会记住自己是哪个线程等待成功的。 - 互斥量的这种特殊性,使得它即使在未触发的状态下,也能为线程所获得。
什么是遗弃问题
- 当线程调用ReleaseMutex的时候,函数会检查调用线程的线程ID是否与互斥量内部保存的线程ID一致。
- 如果一致,递归计数会递减。
- 如果线程ID不一致,那么ReleaseMutex将不执行任何操作并返回FALSE给调用者。
- 这是用GetLastError会返回ERROR_NOT_OWNER(视图释放的互斥量不属于调用者)。
- 因此,如果占用互斥量的线程在释放互斥量之前终止了(使用了ExitThread,TerminateThread,ExitProcess或TernimateProcess),那么对互斥量和正在等待该互斥量的线程来说,将会发生一种情况:
- 系统认为互斥量被遗弃(由于占用互斥量的线程已经终止了,所以再也无法释放它)。
- 因为系统会记录所有的互斥量和线程内核对象,因此它确切的知道互斥量何时被遗弃。
- 当互斥量被遗弃的时候,系统会自动将互斥量的线程ID设为0,并将它的递归计数设为0。
- 然后系统会检查有没有其他线程在等待该互斥量
- 如果有的话,那么系统会“公平的”选择一个正在等待的线程,把对象内部的线程ID设为所选择的那个线程的线程ID,并把递归计数设为1,这样被选择的线程就变为了可调度状态。
- 对于这种情况,一切都和其他情况都一样,不同的是,等待函数不再返回通常的WAIT_OBJECT_0,而是返回一个特殊的值WAIT_ABANDONED,这个特殊的返回值是只适用于互斥量,它表示正在等待的互斥量为其他线程所占用,但该线程在完成对共享资源的使用之前终止了。
- 对于刚获得互斥量的线程来说,这个情况是有点尴尬的,因为它并不知道资源目前正处于什么状态(有可能已经完全被破坏了)。所以这种情况,我们的应用程序必须自己决定怎么处理。
互斥量VS关键段
特征 | 互斥量 | 关键段 |
能否跨进程 | 是 | 否 |
声明 | HANDLE hmtx; | CRITICAL_SECTION cs; |
初始化 | hmtx = CreateMutex(NULL, FALSE, NULL); | InitializeCriticalSection(&cs); |
清理 | CloseHandle(hmtx); | DeleteCriticalSection(&cs); |
无限等待 | WaitForSingleObject(hmtx, INFINITE); | EnterCriticalSection(&cs); |
0等待 | WaitForSingleObject(hmtx, 0); | TryEnterCriticalSection(&cs); |
任意时间长度的等待 | WaitForSingleObject(hmtx, dwMilliseconds); | 不支持 |
释放 | ReleaseMutex(hmtx); | LeaveCriticalSection(&cs); |
是否能同时等待其他内核对象 | 使用WaitForMultipleObjects或类似函数 | 否 |
线程同步对象速查表
对象 | 未触发状态 | 触发状态 | 成功等待的副作用 |
进程 | 进程仍在运行的时候 | 进程终止的时候 | 没有 |
线程 | 线程仍在运行的时候 | 线程终止的时候 | 没有 |
作业 | 作业尚未超时的时候 | 作业超时的时候 | 没有 |
文件 | 有待处理的I/O请求的时候 | I/O请求完成的时候 | 没有 |
控制台输入 | 没有输入的时候 | 有输入的时候 | 没有 |
文件变更通知 | 文件没有变更的时候 | 文件系统检测到文件变更的时候 | 重置通知 |
自动重置事件 | ResetEvent,PulseEvent或者等待成功的时候 | SetEvent/PulseEvent被调用的时候 | 重置事件 |
手动重置事件 | ResetEvent,PulseEvent | SetEvent/PulseEvent被调用的时候 | 没有 |
自动重置可等待计时器 | CancelWaitableTimer或者等待成功的时候 | 事件到的时候(SetWaitableTimer) | 重置计时器 |
手动重置可等待计时器 | CancelWaitableTimer | 事件到的时候(SetWaitableTimer) | 没有 |
信号量 | 等待成功的时候 | 计数大于0的时候(ReleaseSemaphore) | 计数减1 |
互斥量 | 等待成功的时候 | 不为线程占用的时候(ReleaseMutex) | 把所有权交给线程 |
关键段(用户模式) | 等待成功的时候((Try)EnterCriticalSection) | 不为线程占用的时候(LeaveCriticalSection) | 把所有权交给线程 |
SRWLock(用户模式) | 等待成功的时候(AcquireSRWLock(Exclusive)) | 不为线程占用的时候(ReleaseSRWLock(Exclusive)) | 把所有权交给线程 |
条件变量(用户模式) | 等待成功的时候(SleepConditionVaiable*) | 被唤醒的时候(Wake(All)ConditionVariable) | 没有 |
- 需要注意的是InterLocked系列函数(用户模式)从来不会使线程变成不可调度状态,它们只是修改一个值并立即返回。
其他线程同步函数
异步设备I/O
- 异步设备I/O允许线程开始读取操作或写入操作,但不必等待读取操作或写入操作完成。
- 设备对象是可同步的内核对象,意味着我们可以调用WaitForSingleObject,并传入文件句柄、套接字、通信端口等等。
- 当系统执行异步I/O的时候,设备对象处于未触发状态。一旦操作完成,系统会将对象变为触发状态,这样线程就知道操作已经完成了。
WaitForInputIdle
1 2 |
DWORD WiatForInputIdle(HANDLE hProcess, DWORD dwMilliseconds); |
- 这个函数会等待由hProcess所标识的进程,直到创建应用程序第一个窗口的线程中没有待处理的输入为止。
- 这个函数对父进程来说比较有用。
MsgWaitForMultipleObjects(Ex)
1 2 3 4 5 |
DWORD MsgWaitForMultipleObjects(DWORD dwCount, PHANDLE phObjects, BOOL bWaitAll, DWORD dwMilliseconds, DWORD dwWakeMask); |
1 2 3 4 5 |
DWORD MsgWaitForMultipleObjectsEx(DWORD dwCount, PHANDLE phObjects, DWORD dwMilliseconds, DWORD dwWakeMask, DWORD dwFlags); |
- 这些函数与WaitForMultipleObjects类似。不同之处在于,不仅内核对象被触发的时候调用线程会变成可调度状态,而且当窗口消息需要被派送到一个由调用线程创建的窗口时,它们也会变成可调度状态。
- 创建窗口的线程和执行与用户界面相关的任务的线程不应该使用WaitForMultipleObjects,而应该使用MsgWaitForMultipleObjectsEx,原因是前者会妨碍线程对用户在用户界面上的操作进行响应。
WaitForDebugEvent
1 2 |
BOOL WaitForDebugEvent(PDEBUG_EVENT pde, DWORD dwMilliseconds); |
- Windows操作系统内建了绝佳的调试支持。当调试器开始执行的时候,会将自己附着到被调试程序。
- 然后调试器只是在一边闲着,等待操作系统通知它有与被调试程序相关的事件发生。
- 而调试器就是通过这个函数来等待这些事件。当调试器调用这个函数的时候,调试器的线程会被挂起。系统会通过让这个函数返回的方式来通知调试器有调试事件发生。
SignalObjectAndWait
1 2 3 4 |
DWORD SignalObjectAndWait(HANDLE hObjectToSignal, HANDLE hObjectToWaitOn, DWORD dwMilliseconds, BOOL bAlertable); |
- 这个函数会通过一个原子操作来触发一个内核对象并等待另一个内核对象
- 当我们调用这个函数的时候,第一个参数标识的必须是一个互斥量、信号量或事件。
任何其他类型的对象将导致函数返回WAIT_FAILED,调用GetLastError将会返回ERROR_INVALID_HANDLE。 - 该函数会在内部检查对象的类型并分别执行与ReleaseMutex,ReleaseSemaphore或SetEvent等价的操作。
- 第二个参数可以标志下列内核对象中任何一种:
- 互斥量
- 信号量
- 事件
- 计时器
- 进程
- 线程
- 作业
- 控制台输入
- 变更通知
- 第三个参数表示函数最多应该花多长时间来等待对象触发。
- 第四个参数表示,当线程处于等待状态的时候,是否应该能够对添加到队列中的异步调用进行处理。
- 关于返回值:
- WAIT_OBJECT_0
- WAIT_TIMEOUT
- WAIT_FAILED
- WAIT_ABANDONED
- WAIT_IO_COMPLETION
- 出于以下两个原因,这个函数比较受欢迎:
- 如果我们需要触发一个对象并等待另一个对象,让一个函数完成两个操作可以节省处理时间。
- 如果没有这个函数,一个线程就无法知道另一个线程何时处于等待状态。
使用等待链遍历API来检测死锁
等待遍历链记录的同步机制的类型
可能的锁 | 描述 |
关键段 | Windows会记录哪个线程正在占用哪个关键段 |
互斥量 | Windows会记录哪个线程正在占用哪个互斥量,即使是被遗弃的互斥量也不例外 |
进程和线程 | Windows会记录哪个线程正在等待进程终止或线程终止 |
SendMessage调用 | Windows会记录哪个线程正在等待SendMessage调用返回 |
COM初始化和调用 | Windows会记录对CoCreateInstance的调用以及对COM对象的方法的调用 |
高级本地过程调用 | WindowsVista中,新的未公开的内核进程间通信机制 |
等待链
- 所谓等待链是一个序列,在这个序列中线程和同步对象交替出现,每个线程等待它后面的对象,而该对象却为等待链中更后面的线程所占用。
相关函数
1 2 |
HWCT OpenThreadWaitChainSession(DWORD dwFlags, PWAITCHAINCALLBACK callback); |
1 2 3 4 5 6 7 |
BOOL WINAPI GetThreadWaitChain(HWCT hWctSessio, DWORD_PTR pContext, DWORD dwFlags, DWORD TID, PDWORD pNodeCount, PWAITCHAIN_NODE_INFO pNodeInfoArray, LPBOOL pbIsCyle); |
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ WinDbg语法规则07/11
- ♥ x86_64汇编学习记述二08/07
- ♥ Windows 核心编程 _ 进程三06/19
- ♥ Windows进程通信相关03/10
- ♥ WinDbg相关01/12
- ♥ COM组件_101/31