《Windows核心编程》读书笔记九 用内核模式进行线程同步
2017-10-11 11:17
387 查看
第九章 用内核对象进行线程同步
本章内容
9.1 等待函数
9.2 等待成功所引起的副作用
9.3 事件内核对象
9.4 可等待的计时器内核对象
9.5 信号量内核对象
9.6 互斥量内核对象
9.7 线程同步对象速查表
9.8 其他的线程同步函数
用户模式下的线程同步高性能,但是存在一些局限。例如无法进行进程间线程的同步,Iterlocked系函数不会把线程切换到等待状态,
进入临界区无法设置最长等待时间等。
内核对象来进行线程同步,功能强大许多。但是唯一的缺点就是性能。
对于线程内核对象可能处于触发(signaled)和未触发(nosignaled)
进程内核对象,在创建时其内部有一个BOOL变量是FALSE, 单进程终止时该内核对象会变成TRUE 表示已经触发
但是这个过程是不可逆的。
以下列出可能处于未触发也可以处于触发状态的内核对象:
进程,线程,作业,文件以及控制台的标准输入流/输出流/错误流
事件,可等待的计时器, 信号量,互斥量
hObject用来标识等待的内核对象,可以处于触发或未触发状态。
dwMilliseconds 指定线程最多愿意花多长时间来等待对象被触发
一直等待直到目标进程终止
WaitForSingleObject(hProcess, INFINITE);
WaitForSingleObject的返回值表示为什么调用线程又能继续执行了。
以下函数可以等待多个内核对象的触发状态
nCount表示希望函数检查内核对象的数量。 必须在(1~MAXIMUM_WAIT_OBJECTS之间)
lpHandles 指向内核对象句柄的数组
bWaitAll 是否等待全部触发,FALSE 表示只要一个触发即可
dwMilliseconds 等待时间
一个例子说明WaitForMultipleObjects的返回值
如果给bWaitAll传递TRUE那么所有内核对象都触发了以后返回值是 WAIT_OBJECT_0
有些内核对象在WaitForXXX返回以后会被自动设置为非触发状态(例如自动重置事件对象)
例如一个例子
HANDLE h[2];
WaitForMultipleObjects(2, h, TRUE, INFINITE);
1)两个线程执行同样的代码,
2)其中一个事件触发了,两个线程都能检测到,但是由于另外一个事件未触发,所以线程会继续等待
3)另一个事件也触发了,其中一个线程检测到两个事件都触发以后,将两个事件又设置为非触发状态并返回
4)另一个线程会继续等待直到两个事件同时触发为止。
WaitForMultipleObjects是以原子操做的方式工作的,当他检查内核对象的状态时,任何其他线程都不能在背后修改对象的状态。这就防止了死锁的发生。
当一个手动重置事件被触发的时候,正在等待该事件的所有线程都变成可调度状态。
当一个自动重置事件被触发时,只有一个正在等待该事件的线程会变成可调度状态。
事件常用于让一个线程执行初始化工作,然后再触发另一个线程,让它执行剩余的工作。
bManualRest TRUE(手动触发), FALSE(自动触发)
bInitialState 初始状态 TRUE(已触发), FALSE (未触发)
创建以后会返回一个事件内核对象句柄,该事件和当前进程相关联。
还有一个CreateEventEx函数用于创建事件。
dwFlags参数可以接受两个位的掩码
dwDesiredAccess 允许指定在创建事件时返回的句柄对事件有何种访问权限。(ex可以限制权限)
如果要调用SetEvent, ResetEvent, PulseEvent。必须使用
EVENT_MODIFY_STATE
其他进程中的线程可以通过多种方式来访问该事件对象,
CreateEvent并在pszName参数中传入相同的值,
使用继承(子进程继承父进程,两者的句柄表中的位置完全一样)
使用DuplicateHandle
或者调用OpenEvent
设置事件为触发状态
BOOL SetEvent(HANDLE hEvent);
设置事件为非触发状态
BOOL ResetEvent(HANDLE hEvent);
自动事件由于等待成功所引起的副作用的影响,当事件被线程等待以后会自动设置为非触发,因此不需要ResetEvent。
因为这里使用了手动重置事件,所以当主线程准备好数据以后3个子线程都能同时运行。
如果使用自动重置事件,那么3个子线程只会有一个能继续运行。为了让3个子线程都能执行代码,修改了一下3个子线程的代码
这个3个线程都会被系统调用,而且每个线程都能独占的读写资源。
BOOL PulseEvent(HANDLE hEvent);
将一个事件变成触发状态以后立即恢复到未触发状态。
Handshake 示例程序
运行结果
使用CONDITION_VARIABLE实现的handshake例子。虽然用CONDITION_VARIABLE和SRWLOCK效率高,但是使用起来代码的复杂度更高。
Console版本的HandShake
创建一个可等待的计时器
打开一个可等待的计时器
bManualReset表示要创建的是手动重置还是自动重置的计时器。
手动重置,等待该计时器的所有线程都变成可调度状态
自动重置,只有一个等待该计时器的线程会变成可调度状态
调用SetWaitableTimer设置计时器,能让其触发
hTimer :计时器内核对象句柄
pDueTime : 计时器第一次触发的时间应该在什么时候
lPeriod:计时器在第一次触发以后应该以怎样的频度触发。
例如以下代码把计时器第一次触发时间设为2018年1月1日下午1:00,以后没间隔6小时触发一次:
注意这里有一个FILETIME 和LARGE_INTEGER结构转换的问题
因为前者是32位对齐,后者是64位对齐。如果直接传递可能会导致对齐错误抛出一个(EXCEPTION_DATATYPE_MISALIGNMENT异常)
还可以给pDueTime传入一个相对时间,给其传入负值。(100纳秒的整数倍)
1秒 = 1000 毫秒 = 1000 000 微妙 = 10 000 000 (个 100 纳秒)
以下例子把计时器第一次触发时间设置为SetWaitableTimer调用结束的5秒钟后;
对于一次性计时器,只要给lPeriod 传递0 。然后调用CloseHandle关闭计时器即可。 或者调用SetWaitableTimer来重置计时器。
bResume(TRUE) 会使的计算机结束挂起模式(如果机器处于挂起模式下)并唤醒等待该计时器的线程。
FALSE 计时器会被触发,但是在机器继续执行前(挂起状态),被唤醒的线程都得不到CPU时间。
CancelWaitableTimer
取消计时器内核对象句柄所对应的计时器。
另外每次调用SetWaitableTimer都会重置计时器。
一个倒计时的例子。从9倒计时到0
SetWaitableTimer允许传入一个APC过程,触发了计时器会调用该过程。
当计时器触发时,当且仅当SetWaitableTimer调用的线程处于可提醒状态(Alertable stabe)(SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectEx, MsgWaitForMultipleObjectEx,SignalObjectAndWait而进入的状态)
如果非处于可提醒状态,系统不会把计时器的APC函数添加到队列中。
不应该同时使用等待函数又同时以可提醒的方式等待一个计时器。
例如
SetWaitableTimer(hTimer,..., TimerAPCRountine,...);
WaitForSingleObjectEx(hTimer, INFINITE, TRUE);
有一个CreateThreadpoolTimer可以创建线程池函数对应的计时器。
大多数应用程序不使用APC, 而是使用IO完成端口
用户计时器SetTimer :在应用程序中使用大量的用户界面基础设置,从而消费更多的资源。而且通过消息机制触发,只有一个线程能得到通知
(WM_TIMER不一定准时,因为其具有最低的优先级)
可等待计时器是内核对象,可以在多个线程间共享。多个线程可以得到通知。
如果当前资源计数大于0,信号量处于触发状态。
如果当前资源计数等于0,信号量处于未触发状态。
创建信号量
psa pszName 前面讲过了。
dwFlags是系统保留的设为0.
参数lMaximumCount 系统能够处理的资源的最大数量
lInitialCoun 初始化有多少资源可用。
例如给服务器进程初始化,没有客户端请求,因此使用一下代码来调用CreateSemaphore
HANDLE hSemaphore = CreateSemaphore(NULL, 0, 5, NULL);
打开一个信号量
dwDesiredAccess参数指定访问权限
线程通过调用ReleaseSemahore来递增信号量的当前资源计数:
lReleaseCount 的值会加到信号量当前资源计数上。
互斥量与临界区的行为完全相同。(内部保护递归计数)
互斥量的规则:
1)如果线程ID为0,那么互斥量不为任何线程所占用,它处于触发状态
2)如果线程ID为非零值,那么一个线程已经占用了该互斥量,它处与未触发状态。
3)与所有其他内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规的规则。(递归计数器的存在,运行同一个线程ID多次进入)
创建互斥量
bInitialOwner控制互斥量的初始状态。FALSE, 互斥量的线程ID和递归计数都被设为0.处于触发状态。
给bInitialOwner穿TRUE,那么对象的线程ID被设为调用线程的ID,递归计数器被设为1.(未触发状态)
或者使用CreateMutexEx
dwDesiredAccess指定访问权限
dwFlags(代替bInitialOwned) 0表示FALSE, CREATE_MUTEX_INITIAL_OWNER等价于TRUE
另一个进程可以调用OpenMutex来得到一个已经存在的互斥量句柄。
BOOL ReleaseMutex(HANDLE hMutex);
使互斥量对象的递归计数器减1, 当递归计数器为0时,还会设置线程ID为0,这就触发了对象。
如果占用互斥量的线程在释放互斥量之前终止(ExitThread ,TerminateThread ,ExitProcess 或TerminateProcess)
系统认为互斥量被遗弃(abandoned) 此时会自动将互斥量线程ID设为0,递归计数器设为0,并检查有没有正在等待该互斥量的线程。
等待函数返回WAIT_ABANDONED(只适用互斥量)
互斥量的任意时间长度等待修正为: WaitForSingleObject(hmtx, dwMilliseconds);
设备对象是是可同步的内核对象,可以调用WaitForSingleObject并传入句柄,套接字,通信端口等。
常用于等待子进程,父进程知道子进程已经初始化完毕的唯一方法,就是等待子进程,直到它不再处理任何输入为止。
当我们要强制在应用程序中输入一些按键的时候,也可以使用WaitForInputIdle。
当向目标进程发送一系列按键消息以后,调用WaitForInputIdle等待其处理完按键消息,然后再发送后续的按键消息。
函数功能:阻塞时仍可以响应消息
MsgWaitForMultipleObjects()函数类似WaitForMultipleObjects(),
但它会在“对象被激发”或“消息到达队列”时被唤醒而返回。
MsgWaitForMultipleObjects()多接收一个参数,允许指定哪些消息是观察对象。
一个应用的例子 该函数同时等待对象,若有消息到底也返回。运行主线程处理消息后继续等待。
hObjectToSignal必须是一个互斥量,信号量或事件。(其他任何对象将导致函数返回WAIT_FAILD)调用GetLastError返回ERROR_INVALID_HANDLE.
hObjectToWaitOn可以是互斥量,信号量,事件,进程,线程,作业,控制台输入变更通知,作业。等等
dwMilliseconds 函数最多花多长时间来等待。
bAlertable表示当线程处于等待状态的时候,是否能够堆添加到队列中的异步过程调用进行处理。
返回值:WAIT_OBJECT_0, WAIT_TIMEOUT, WAIT_FAILED, WAIT)ABANDONED, WAIT_IO_COMPLETION
配合PulseEvent使用。
SignalObjectAndWait释放一个对象,同时立即等待(原子方式)
能确保其100%能看见别的线程调用的PulseEvent。
LockCop示例程序
LockCop展示如何使用WCT函数来创建一个非常有用的工具。
等待链
一条等待链是一个序列,在这个序列中线程和同步对象交替出现,每个线程等待它后面的对象,而该对象却为等待链中更后面的线程所占用。
例如3212正在等待线程2260释放一个关键段,而线程2260正在等待线程3212释放另外一个关键段。这就是典型的死锁。
代码LockCop.cpp
运行结果(检测例子BadLock)
LockCop不支持WaitForMultileObjects
本章内容
9.1 等待函数
9.2 等待成功所引起的副作用
9.3 事件内核对象
9.4 可等待的计时器内核对象
9.5 信号量内核对象
9.6 互斥量内核对象
9.7 线程同步对象速查表
9.8 其他的线程同步函数
用户模式下的线程同步高性能,但是存在一些局限。例如无法进行进程间线程的同步,Iterlocked系函数不会把线程切换到等待状态,
进入临界区无法设置最长等待时间等。
内核对象来进行线程同步,功能强大许多。但是唯一的缺点就是性能。
对于线程内核对象可能处于触发(signaled)和未触发(nosignaled)
进程内核对象,在创建时其内部有一个BOOL变量是FALSE, 单进程终止时该内核对象会变成TRUE 表示已经触发
但是这个过程是不可逆的。
以下列出可能处于未触发也可以处于触发状态的内核对象:
进程,线程,作业,文件以及控制台的标准输入流/输出流/错误流
事件,可等待的计时器, 信号量,互斥量
9.1 等待函数
等待函数使一个线程资源进入等待状态,直到指定的内核对象被触发为止。如果在调用一个等待函数时,响应的内核对象已经处于触发状态,那么线程是不会进入等待状态的。WINBASEAPI DWORD WINAPI WaitForSingleObject( _In_ HANDLE hHandle, _In_ DWORD dwMilliseconds );
hObject用来标识等待的内核对象,可以处于触发或未触发状态。
dwMilliseconds 指定线程最多愿意花多长时间来等待对象被触发
一直等待直到目标进程终止
WaitForSingleObject(hProcess, INFINITE);
DWORD dw = WaitForSingleObject(hProcess, 5000); switch (dw) { case WAIT_OBJECT_0: // The process terminated. break; case WAIT_TIMEOUT: // The process did not terminate within 5000 milliseconds. break; case WAIT_FAILED: // Bad call to function (invalid handle?) break; }
WaitForSingleObject的返回值表示为什么调用线程又能继续执行了。
以下函数可以等待多个内核对象的触发状态
WINBASEAPI DWORD WINAPI WaitForMultipleObjects( _In_ DWORD nCount, _In_reads_(nCount) CONST HANDLE *lpHandles, _In_ BOOL bWaitAll, _In_ DWORD dwMilliseconds );
nCount表示希望函数检查内核对象的数量。 必须在(1~MAXIMUM_WAIT_OBJECTS之间)
lpHandles 指向内核对象句柄的数组
bWaitAll 是否等待全部触发,FALSE 表示只要一个触发即可
dwMilliseconds 等待时间
一个例子说明WaitForMultipleObjects的返回值
HANDLE h[3]; h[0] = hProcess1; h[1] = hProcess2; h[2] = hProcess3; DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000); switch (dw) { case WAIT_FAILED: // Bad call to function (invalid handle?) break; case WAIT_TIMEOUT: // None of the objects became signaled within 5000 milliseconds. break; case WAIT_OBJECT_0 + 0: // The process identified by h[0] (hProcess1) terminated. break; case WAIT_OBJECT_0 + 1: // The process identified by h[1] (hProcess1) terminated. break; case WAIT_OBJECT_0 + 2: // The process identified by h[2] (hProcess1) terminated. break; }
如果给bWaitAll传递TRUE那么所有内核对象都触发了以后返回值是 WAIT_OBJECT_0
9.2 等待成功锁引起的副作用
如果WaitForXXXObject成功返回,那么传入的句柄对象发生了变化,成为“等待成功锁引起的副作用”。有些内核对象在WaitForXXX返回以后会被自动设置为非触发状态(例如自动重置事件对象)
例如一个例子
HANDLE h[2];
WaitForMultipleObjects(2, h, TRUE, INFINITE);
1)两个线程执行同样的代码,
2)其中一个事件触发了,两个线程都能检测到,但是由于另外一个事件未触发,所以线程会继续等待
3)另一个事件也触发了,其中一个线程检测到两个事件都触发以后,将两个事件又设置为非触发状态并返回
4)另一个线程会继续等待直到两个事件同时触发为止。
WaitForMultipleObjects是以原子操做的方式工作的,当他检查内核对象的状态时,任何其他线程都不能在背后修改对象的状态。这就防止了死锁的发生。
9.3 事件内核对象
事件包含一个使用计数器,一个用来标识事件是自动重置还是手动重置的布尔值,以及另一个布尔值用来表示事件有没有被触发。当一个手动重置事件被触发的时候,正在等待该事件的所有线程都变成可调度状态。
当一个自动重置事件被触发时,只有一个正在等待该事件的线程会变成可调度状态。
事件常用于让一个线程执行初始化工作,然后再触发另一个线程,让它执行剩余的工作。
WINBASEAPI _Ret_maybenull_ HANDLE WINAPI CreateEventW( _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, _In_ BOOL bManualReset, _In_ BOOL bInitialState, _In_opt_ LPCWSTR lpName );
bManualRest TRUE(手动触发), FALSE(自动触发)
bInitialState 初始状态 TRUE(已触发), FALSE (未触发)
创建以后会返回一个事件内核对象句柄,该事件和当前进程相关联。
还有一个CreateEventEx函数用于创建事件。
WINBASEAPI _Ret_maybenull_ HANDLE WINAPI CreateEventExW( _In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, _In_opt_ LPCWSTR lpName, _In_ DWORD dwFlags, _In_ DWORD dwDesiredAccess );
dwFlags参数可以接受两个位的掩码
dwDesiredAccess 允许指定在创建事件时返回的句柄对事件有何种访问权限。(ex可以限制权限)
如果要调用SetEvent, ResetEvent, PulseEvent。必须使用
EVENT_MODIFY_STATE
其他进程中的线程可以通过多种方式来访问该事件对象,
CreateEvent并在pszName参数中传入相同的值,
使用继承(子进程继承父进程,两者的句柄表中的位置完全一样)
使用DuplicateHandle
或者调用OpenEvent
WINBASEAPI _Ret_maybenull_ HANDLE WINAPI OpenEventW( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ LPCWSTR lpName );
设置事件为触发状态
BOOL SetEvent(HANDLE hEvent);
设置事件为非触发状态
BOOL ResetEvent(HANDLE hEvent);
自动事件由于等待成功所引起的副作用的影响,当事件被线程等待以后会自动设置为非触发,因此不需要ResetEvent。
HANDLE g_hEvent; DWORD WINAPI WordCount(PVOID pvParam) { // Wait until the file's data is in memory. WaitForSingleObject(g_hEvent, INFINITE); // Access the memory block. // ... return 0; } DWORD WINAPI SpellCheck(PVOID pvParam) { // Wait until the file's data is in memory. WaitForSingleObject(g_hEvent, INFINITE); // Access the memory block. // ... return 0; } DWORD WINAPI GrammarCheck(PVOID pvParam) { // Wait until the file's data is in memory. WaitForSingleObject(g_hEvent, INFINITE); // Access the memory block. // ... return 0; } int _tmain(int argc, TCHAR* argv[], TCHAR * env[]) { // Crate the manual-reset, nosignaled event. g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); // Spawn 3 new threads. HANDLE hThread[3]; DWORD dwThreadID; hThread[0] = CreateThread(NULL, 0, WordCount, NULL, 0, &dwThreadID); hThread[1] = CreateThread(NULL, 0, SpellCheck, NULL, 0, &dwThreadID); hThread[2] = CreateThread(NULL, 0, GrammarCheck, NULL, 0, &dwThreadID); OpenFileAndReadContentsIntoMemory(...); // Allow all 3 threads to access the memory. SetEvent(g_hEvent); return 0; }
因为这里使用了手动重置事件,所以当主线程准备好数据以后3个子线程都能同时运行。
如果使用自动重置事件,那么3个子线程只会有一个能继续运行。为了让3个子线程都能执行代码,修改了一下3个子线程的代码
DWORD WINAPI WordCount(PVOID pvParam) { // Wait until the file's data is in memory. WaitForSingleObject(g_hEvent, INFINITE); // Access the memory block. // ... SetEvent(g_hEvent); return 0; } DWORD WINAPI SpellCheck(PVOID pvParam) { // Wait until the file's data is in memory. WaitForSingleObject(g_hEvent, INFINITE); // Access the memory block. // ... SetEvent(g_hEvent); return 0; } DWORD WINAPI GrammarCheck(PVOID pvParam) { // Wait until the file's data is in memory. WaitForSingleObject(g_hEvent, INFINITE); // Access the memory block. // ... SetEvent(g_hEvent); return 0; }
这个3个线程都会被系统调用,而且每个线程都能独占的读写资源。
BOOL PulseEvent(HANDLE hEvent);
将一个事件变成触发状态以后立即恢复到未触发状态。
Handshake 示例程序
/****************************************************************************** Module: Handshake.cpp Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre ******************************************************************************/ #include "..\CommonFiles\CmnHdr.h" #include <windowsx.h> #include <tchar.h> #include "Resource.h" ////////////////////////////////////////////////////////////////////////// #define BUFFERSIZ 1024 // This event is signaled when the client has a request for the server HANDLE g_hevtRequestSubmitted; // This event is signaled when the server has a result for the client HANDLE g_hevtResultReturned; // The buffer shared between the client and server threads TCHAR g_szSharedRequestAndResultBuffer[BUFFERSIZ]; // The special value sent from the client that causes the // server thread to terminate cleanly. TCHAR g_szServerShutdown[] = TEXT("Server Shutdown"); // The server thread will check that the main dialog is no longer alive // When the shutdown message is received. HWND g_hMainDlg; ////////////////////////////////////////////////////////////////////////// // This is the code executed by the server thread DWORD WINAPI ServerThread(PVOID pvParam) { // Assume that the server thread is to run forever BOOL fShutdown = FALSE; while (!fShutdown) { // Wait for the client to submit a request WaitForSingleObject(g_hevtRequestSubmitted, INFINITE); // Check to see if the client wants the server to terminate fShutdown = (g_hMainDlg == NULL) && (_tcscmp(g_szSharedRequestAndResultBuffer, g_szServerShutdown) == 0); if (!fShutdown) { // Process the client's request (reverse the string) _tcsrev(g_szSharedRequestAndResultBuffer); } // Let the client process the request's result SetEvent(g_hevtResultReturned); } // The client wants us to shut down, exit return 0; } ////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) { chSETDLGICONS(hwnd, IDI_HANDSHAKE); // Initialize the edit control with some test data request Edit_SetText(GetDlgItem(hwnd, IDC_REQUEST), TEXT("Some test data")); // Store the main dialog window handle g_hMainDlg = hwnd; return TRUE; } void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; case IDC_SUBMIT: // Submit a request to the server thread // Copy the request string into the shared data buffer Edit_GetText(GetDlgItem(hwnd, IDC_REQUEST), g_szSharedRequestAndResultBuffer, _countof(g_szSharedRequestAndResultBuffer)); // Let the server thread know that a request is ready in the buffer // Wait for the server to process the request and give us the result SignalObjectAndWait(g_hevtRequestSubmitted, g_hevtResultReturned, INFINITE, false); // Let the user know the result Edit_SetText(GetDlgItem(hwnd, IDC_RESULT), g_szSharedRequestAndResultBuffer); break; } } ////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); } return FALSE; } ////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hInstanceExe, HINSTANCE, PTSTR, int) { // Create & initialize the 2 nonsignaled, auto-reset events g_hevtRequestSubmitted = CreateEvent(NULL, FALSE, FALSE, NULL); g_hevtResultReturned = CreateEvent(NULL, FALSE, FALSE, NULL); // Spawn the server thread DWORD dwThreadID; HANDLE hThreadServer = chBEGINTHREADEX(NULL, 0, ServerThread, NULL, 0, &dwThreadID); // Execute the client thread's user interface DialogBox(hInstanceExe, MAKEINTRESOURCE(IDD_HANDSHAKE), NULL, Dlg_Proc); g_hMainDlg = NULL; // The client's UI is closing, have the server thread shutdown _tcscpy_s(g_szSharedRequestAndResultBuffer, _countof(g_szSharedRequestAndResultBuffer), g_szServerShutdown); SetEvent(g_hevtRequestSubmitted); HANDLE h[2]; h[0] = g_hevtResultReturned; h[1] = hThreadServer; WaitForMultipleObjects(2, h, TRUE, INFINITE); // Properly clean up everything CloseHandle(hThreadServer); CloseHandle(g_hevtRequestSubmitted); CloseHandle(g_hevtResultReturned); // The client thread terminates with the whole process return 0; }
运行结果
使用CONDITION_VARIABLE实现的handshake例子。虽然用CONDITION_VARIABLE和SRWLOCK效率高,但是使用起来代码的复杂度更高。
Console版本的HandShake
#define _CRT_SECURE_NO_WARNINGS #include <tchar.h> #include <windows.h> #include <stdio.h> #include <Shlobj.h> #include <strsafe.h> #include <malloc.h> #include <process.h> #include <winnt.h> #include <ctype.h> CONDITION_VARIABLE g_cvReturnResult; CONDITION_VARIABLE g_cvSubmitRequest; SRWLOCK g_srwLock; TCHAR g_szSharedBuffer[BUFSIZ]; TCHAR g_szShutdown[] = TEXT("Server Shutdown"); unsigned __stdcall serverThread(void * pParam) { bool bShutdown = false; _tprintf(TEXT("Server Started! \n")); while (!bShutdown) { AcquireSRWLockExclusive(&g_srwLock); SleepConditionVariableSRW(&g_cvSubmitRequest, &g_srwLock, INFINITE, 0); // for Exclusive SRWLock bShutdown = (_tcscmp(g_szSharedBuffer, g_szShutdown) == 0); if (!bShutdown) _tcsrev(g_szSharedBuffer); ReleaseSRWLockExclusive(&g_srwLock); WakeConditionVariable(&g_cvReturnResult); } _tprintf(TEXT("Server exit with 0! \n")); return 0; } int _tmain(int argc, TCHAR* argv[], TCHAR * env[]) { // Init Condition variable and srwlock InitializeConditionVariable(&g_cvReturnResult); InitializeConditionVariable(&g_cvSubmitRequest); InitializeSRWLock(&g_srwLock); _tprintf(TEXT("Handshake Console Version v 0.1\n")); unsigned int iThreadID; HANDLE hThreadServer = (HANDLE)_beginthreadex(NULL, 0, serverThread, NULL, 0, &iThreadID); bool bShutdown = false; while (!bShutdown) { _tprintf(TEXT("Please input the request:\n")); #ifdef UNICODE _getws(g_szSharedBuffer); #else gets(g_szSharedBuffer); #endif bShutdown = (_tcscmp(g_szSharedBuffer, g_szShutdown) == 0); if (!bShutdown) { AcquireSRWLockExclusive(&g_srwLock); WakeConditionVariable(&g_cvSubmitRequest); // signal the server thread SleepConditionVariableSRW(&g_cvReturnResult, &g_srwLock, INFINITE, 0); _tprintf(TEXT("Result:\t%s\n"), g_szSharedBuffer); ReleaseSRWLockExclusive(&g_srwLock); } else { WakeConditionVariable(&g_cvSubmitRequest); // signal the server thread to shutdown. } } WaitForSingleObject(hThreadServer, INFINITE); // Clean UP CloseHandle(hThreadServer); system("pause"); return 0; }
9.4 可等待的计时器内核对象
可等待的计时器是这样一种内核对象:他们会在某个指定的时间触发,或每间隔一段时间触发。创建一个可等待的计时器
WINBASEAPI _Ret_maybenull_ HANDLE WINAPI CreateWaitableTimerW( _In_opt_ LPSECURITY_ATTRIBUTES lpTimerAttributes, _In_ BOOL bManualReset, _In_opt_ LPCWSTR lpTimerName );
打开一个可等待的计时器
WINBASEAPI _Ret_maybenull_ HANDLE WINAPI OpenWaitableTimerW( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ LPCWSTR lpTimerName );
bManualReset表示要创建的是手动重置还是自动重置的计时器。
手动重置,等待该计时器的所有线程都变成可调度状态
自动重置,只有一个等待该计时器的线程会变成可调度状态
调用SetWaitableTimer设置计时器,能让其触发
WINBASEAPI BOOL WINAPI SetWaitableTimer( _In_ HANDLE hTimer, _In_ const LARGE_INTEGER * lpDueTime, _In_ LONG lPeriod, _In_opt_ PTIMERAPCROUTINE pfnCompletionRoutine, _In_opt_ LPVOID lpArgToCompletionRoutine, _In_ BOOL fResume );
hTimer :计时器内核对象句柄
pDueTime : 计时器第一次触发的时间应该在什么时候
lPeriod:计时器在第一次触发以后应该以怎样的频度触发。
例如以下代码把计时器第一次触发时间设为2018年1月1日下午1:00,以后没间隔6小时触发一次:
// Declare our local variables. HANDLE hTimer; SYSTEMTIME st; FILETIME ftLocal, ftUTC; LARGE_INTEGER liUTC; // Create an auto-reset timer. hTimer = CreateWaitableTimer(NULL, FALSE, NULL); // First signaling is a January 1, 2018, at 1:00 P.M. (local time). st.wYear = 2018; // Year st.wMonth = 1; // January st.wDayOfWeek = 0; // Ignored st.wDay = 1; // The first of the month st.wHour = 13; // 1PM st.wMinute = 0; // 0 minutes into the hour st.wSecond = 0; // 0 seconds into the minute st.wMilliseconds = 0; // 0 milliseconds into the second SystemTimeToFileTime(&st, &ftLocal); // Convet local time to UTC time. LocalFileTimeToFileTime(&ftLocal, &ftUTC); // Convert FILETIME to LARGE_INTEGER because of different alignment. liUTC.LowPart = ftUTC.dwLowDateTime; liUTC.HighPart = ftUTC.dwHighDateTime; // Set the timer. SetWaitableTimer(hTimer, &liUTC, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);
注意这里有一个FILETIME 和LARGE_INTEGER结构转换的问题
因为前者是32位对齐,后者是64位对齐。如果直接传递可能会导致对齐错误抛出一个(EXCEPTION_DATATYPE_MISALIGNMENT异常)
还可以给pDueTime传入一个相对时间,给其传入负值。(100纳秒的整数倍)
1秒 = 1000 毫秒 = 1000 000 微妙 = 10 000 000 (个 100 纳秒)
以下例子把计时器第一次触发时间设置为SetWaitableTimer调用结束的5秒钟后;
// Declare our local variables. HANDLE hTimer; LARGE_INTEGER li; // Create an auto-reset timer. hTimer = CreateWaitableTimer(NULL, FALSE, NULL); // Set the timer to go off 5 seconds after calling SetWaitableTimer. // Timer unit is 100 nanoseconds. const int nTimerUnitsPerSecond = 10000000; // Negate the time so that SetWaitableTimer knows we // want relative time instead of absolute time. li.QuadPart = -(5 * nTimerUnitsPerSecond); // Set the timer. SetWaitableTimer(hTimer, &li, 6 * 60 * 60 * 1000, NULL, NULL, FALSE);
对于一次性计时器,只要给lPeriod 传递0 。然后调用CloseHandle关闭计时器即可。 或者调用SetWaitableTimer来重置计时器。
bResume(TRUE) 会使的计算机结束挂起模式(如果机器处于挂起模式下)并唤醒等待该计时器的线程。
FALSE 计时器会被触发,但是在机器继续执行前(挂起状态),被唤醒的线程都得不到CPU时间。
CancelWaitableTimer
WINBASEAPI BOOL WINAPI CancelWaitableTimer( _In_ HANDLE hTimer );
取消计时器内核对象句柄所对应的计时器。
另外每次调用SetWaitableTimer都会重置计时器。
一个倒计时的例子。从9倒计时到0
#define _CRT_SECURE_NO_WARNINGS #include <tchar.h> #include <windows.h> #include <stdio.h> #include <Shlobj.h> #include <strsafe.h> #include <malloc.h> #include <process.h> #include <winnt.h> #include <ctype.h> // Declare our local variables. HANDLE hTimer; unsigned __stdcall OnTimer(void * param) { int nCount = 10; while (nCount--) { WaitForSingleObject(hTimer, INFINITE); _tprintf(TEXT("count:\t%d\n"), nCount); } return 0; } int _tmain(int argc, TCHAR* argv[], TCHAR * env[]) { LARGE_INTEGER li; // Create an auto-reset timer. hTimer = CreateWaitableTimer(NULL, FALSE, NULL); // Set the timer to go off 5 seconds after calling SetWaitableTimer. // Timer unit is 100 nanoseconds. const int nTimerUnitsPerSecond = 10000000; // Negate the time so that SetWaitableTimer knows we // want relative time instead of absolute time. li.QuadPart = -(5 * nTimerUnitsPerSecond); // Set the timer. SetWaitableTimer(hTimer, &li, 1000, NULL, NULL, FALSE); // Create the OnTimer Thread unsigned int ThreadID; HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, OnTimer, NULL, NULL, &ThreadID); WaitForSingleObject(hThread, INFINITE); system("pause"); return 0; }
9.4.1 让可等待的计时器添加APC调用
APC异步过程调用(asynchronous procedure call)SetWaitableTimer允许传入一个APC过程,触发了计时器会调用该过程。
当计时器触发时,当且仅当SetWaitableTimer调用的线程处于可提醒状态(Alertable stabe)(SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectEx, MsgWaitForMultipleObjectEx,SignalObjectAndWait而进入的状态)
如果非处于可提醒状态,系统不会把计时器的APC函数添加到队列中。
#define _CRT_SECURE_NO_WARNINGS #include <tchar.h> #include <windows.h> #include <stdio.h> #include <Shlobj.h> #include <strsafe.h> #include <malloc.h> #include <process.h> #include <winnt.h> #include <ctype.h> #include <io.h> #include <fcntl.h> VOID APIENTRY TimerAPCRountine(PVOID pvArgToCompletionRountine, DWORD dwTimerLowValue, DWORD dwTimerHighValue) { FILETIME ftUTC, ftLocal; SYSTEMTIME st; TCHAR szBuf[256]; // Put the time in a FILETIME structure. ftUTC.dwLowDateTime = dwTimerLowValue; ftUTC.dwHighDateTime = dwTimerHighValue; // Convert the UTC time to the user's local time. FileTimeToLocalFileTime(&ftUTC, &ftLocal); // Convert the FILETIME to the SYSTEMTIME structure // required by GetDateFormat and GetTimeFormat. FileTimeToSystemTime(&ftLocal, &st); // Construct a string with the // date/time that the timer went off. 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); _tprintf(TEXT("%s\n"), szBuf); } void SomeFunc() { // Create a timer. (It doesn't matter whether it's manual-reset // or auto-reset.) HANDLE hTimer = CreateWaitableTimer(NULL, TRUE, NULL); // Set timer to go off in 5 seconds. LARGE_INTEGER li = { 0 }; SetWaitableTimer(hTimer, &li, 5000, TimerAPCRountine, NULL, FALSE); // Wait in an alertable state for the timer to go off. SleepEx(INFINITE, TRUE); CloseHandle(hTimer); } int _tmain(int argc, TCHAR* argv[], TCHAR * env[]) { _setmode(_fileno(stdout), _O_WTEXT); SomeFunc(); system("pause"); return 0; }
不应该同时使用等待函数又同时以可提醒的方式等待一个计时器。
例如
SetWaitableTimer(hTimer,..., TimerAPCRountine,...);
WaitForSingleObjectEx(hTimer, INFINITE, TRUE);
9.4.2 计时器的剩余问题
在通信协议中会大量用到计时器,但是通常为每个请求创建计时器内核对象,将严重影响系统性能。有一个CreateThreadpoolTimer可以创建线程池函数对应的计时器。
大多数应用程序不使用APC, 而是使用IO完成端口
用户计时器SetTimer :在应用程序中使用大量的用户界面基础设置,从而消费更多的资源。而且通过消息机制触发,只有一个线程能得到通知
(WM_TIMER不一定准时,因为其具有最低的优先级)
可等待计时器是内核对象,可以在多个线程间共享。多个线程可以得到通知。
9.5 信号量内核对象
信号量内核对象用来对资源进行计数,除了使用计数器。还包含(32bit值)一个最大资源计数和当前资源计数。如果当前资源计数大于0,信号量处于触发状态。
如果当前资源计数等于0,信号量处于未触发状态。
创建信号量
WINBASEAPI _Ret_maybenull_ HANDLE WINAPI CreateSemaphoreW( _In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, _In_ LONG lInitialCount, _In_ LONG lMaximumCount, _In_opt_ LPCWSTR lpName );
psa pszName 前面讲过了。
dwFlags是系统保留的设为0.
参数lMaximumCount 系统能够处理的资源的最大数量
lInitialCoun 初始化有多少资源可用。
例如给服务器进程初始化,没有客户端请求,因此使用一下代码来调用CreateSemaphore
HANDLE hSemaphore = CreateSemaphore(NULL, 0, 5, NULL);
打开一个信号量
WINBASEAPI _Ret_maybenull_ HANDLE WINAPI OpenSemaphoreW( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ LPCWSTR lpName );
dwDesiredAccess参数指定访问权限
线程通过调用ReleaseSemahore来递增信号量的当前资源计数:
WINBASEAPI BOOL WINAPI ReleaseSemaphore( _In_ HANDLE hSemaphore, _In_ LONG lReleaseCount, _Out_opt_ LPLONG lpPreviousCount );
lReleaseCount 的值会加到信号量当前资源计数上。
9.6 互斥量内核对象
互斥量(mutex)内核对象用来确保一个线程独占一个资源的访问。互斥量与临界区的行为完全相同。(内部保护递归计数)
互斥量的规则:
1)如果线程ID为0,那么互斥量不为任何线程所占用,它处于触发状态
2)如果线程ID为非零值,那么一个线程已经占用了该互斥量,它处与未触发状态。
3)与所有其他内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规的规则。(递归计数器的存在,运行同一个线程ID多次进入)
创建互斥量
WINBASEAPI _Ret_maybenull_ HANDLE WINAPI CreateMutexW( _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, _In_ BOOL bInitialOwner, _In_opt_ LPCWSTR lpName );
bInitialOwner控制互斥量的初始状态。FALSE, 互斥量的线程ID和递归计数都被设为0.处于触发状态。
给bInitialOwner穿TRUE,那么对象的线程ID被设为调用线程的ID,递归计数器被设为1.(未触发状态)
或者使用CreateMutexEx
WINBASEAPI _Ret_maybenull_ HANDLE WINAPI CreateMutexExW( _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, _In_opt_ LPCWSTR lpName, _In_ DWORD dwFlags, _In_ DWORD dwDesiredAccess );
dwDesiredAccess指定访问权限
dwFlags(代替bInitialOwned) 0表示FALSE, CREATE_MUTEX_INITIAL_OWNER等价于TRUE
另一个进程可以调用OpenMutex来得到一个已经存在的互斥量句柄。
WINBASEAPI _Ret_maybenull_ HANDLE WINAPI OpenMutexW( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ LPCWSTR lpName );
BOOL ReleaseMutex(HANDLE hMutex);
使互斥量对象的递归计数器减1, 当递归计数器为0时,还会设置线程ID为0,这就触发了对象。
9.6.1 遗弃问题
互斥量具有线程所有权的功能,即使未触发也能多次进入。如果占用互斥量的线程在释放互斥量之前终止(ExitThread ,TerminateThread ,ExitProcess 或TerminateProcess)
系统认为互斥量被遗弃(abandoned) 此时会自动将互斥量线程ID设为0,递归计数器设为0,并检查有没有正在等待该互斥量的线程。
等待函数返回WAIT_ABANDONED(只适用互斥量)
9.6.2 互斥量与关键段(临界区)的对比
互斥量的任意时间长度等待修正为: WaitForSingleObject(hmtx, dwMilliseconds);
9.6.3 Queue 示例程序
本章的Queue使用了互斥量和信号量来对一个队列的简单数据元素进行控制。/****************************************************************************** Module: Queue.cpp Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre ******************************************************************************/ #include "..\CommonFiles\CmnHdr.h" #include <windowsx.h> #include <tchar.h> #include <StrSafe.h> #include "Resource.h" ////////////////////////////////////////////////////////////////////////// class CQueue { public: struct ELEMENT { int m_nThreadNum, m_nRequestNum; // Other element data should go there }; typedef ELEMENT * PELEMENT; private: PELEMENT m_pElements; // Array of elements to be processed int m_nMaxElements; // Maximum # of elements in the array HANDLE m_h[2]; // Mutex & semaphore handles HANDLE &m_hmtxQ; // Reference to m_h[0] HANDLE &m_hsemNumElements; // Reference to m_h[1] public: CQueue(int nMaxElements); ~CQueue(); BOOL Append(PELEMENT pElement, DWORD dwMilliseconds); BOOL Remove(PELEMENT pElement, DWORD dwMilliseconds); }; ////////////////////////////////////////////////////////////////////////// CQueue::CQueue(int nMaxElements) : m_hmtxQ(m_h[0]), m_hsemNumElements(m_h[1]) { m_pElements = (PELEMENT) HeapAlloc(GetProcessHeap(), 0, sizeof(ELEMENT) * nMaxElements); m_nMaxElements = nMaxElements; m_hmtxQ = CreateMutex(NULL, FALSE, NULL); m_hsemNumElements = CreateSemaphore(NULL, 0, nMaxElements, NULL); } ////////////////////////////////////////////////////////////////////////// CQueue::~CQueue() { CloseHandle(m_hsemNumElements); CloseHandle(m_hmtxQ); HeapFree(GetProcessHeap(), 0, m_pElements); } ////////////////////////////////////////////////////////////////////////// BOOL CQueue::Append(PELEMENT pElement, DWORD dwTimeout) { BOOL fOk = FALSE; DWORD dw = WaitForSingleObject(m_hmtxQ, dwTimeout); if (dw == WAIT_OBJECT_0) { // This thread has exclusive access to the queue // Increment the number of elements in the queue LONG lPrevCount; fOk = ReleaseSemaphore(m_hsemNumElements, 1, &lPrevCount); if (fOk) { // The queue is not full, append the new element m_pElements[lPrevCount] = *pElement; } else { // The queue is full, set the error code and return failure SetLastError(ERROR_DATABASE_FULL); } // Allow other threads to access the queue ReleaseMutex(m_hmtxQ); } else { // Timeout, set error code and return failure SetLastError(ERROR_TIMEOUT); } return fOk; // Call GetLastError for more info } ////////////////////////////////////////////////////////////////////////// BOOL CQueue::Remove(PELEMENT pElement, DWORD dwTimeout) { // Wait for exclusive access to queue and for queue to have element. BOOL fOk = (WaitForMultipleObjects(_countof(m_h), m_h, TRUE, dwTimeout) == WAIT_OBJECT_0); if (fOk) { // The queue has an element, pull it from the queue *pElement = m_pElements[0]; // Shift the remaining elements down MoveMemory(&m_pElements[0], &m_pElements[1], sizeof(ELEMENT) * (m_nMaxElements - 1)); // Allow other threads to access the queue ReleaseMutex(m_hmtxQ); } else { // Timeout, set error code and return failure SetLastError(ERROR_TIMEOUT); } return fOk; // Call GetLastError for more info } ////////////////////////////////////////////////////////////////////////// CQueue g_q(10); // The shared queue volatile LONG g_fShutdown = FALSE; // Signals client/server threads to die HWND g_hwnd; // How client/server threads give status // Handles to all client/server threads & number of client/server threads HANDLE g_hThreads[MAXIMUM_WAIT_OBJECTS]; int g_nNumThreads = 0; ////////////////////////////////////////////////////////////////////////// DWORD WINAPI ClientThread(PVOID pvParam) { int nThreadNum = PtrToUlong(pvParam); HWND hwndLB = GetDlgItem(g_hwnd, IDC_CLIENTS); int nRequestNum = 0; while (1 != InterlockedCompareExchange(&g_fShutdown, 0, 0)) { // Keep track of the current processed element nRequestNum++; TCHAR sz[1024]; CQueue::ELEMENT e = { nThreadNum, nRequestNum }; // Try to put an element on the queue if (g_q.Append(&e, 200)) { // Indicate which thread sent it and which request StringCchPrintf(sz, _countof(sz), TEXT("Sending %d:%d"), nThreadNum, nRequestNum); } else { // Couldn't put an element on the queue StringCchPrintf(sz, _countof(sz), TEXT("Sending %d:%d (%s)"), nThreadNum, nRequestNum, (GetLastError() == ERROR_TIMEOUT) ? TEXT("timeout") : TEXT("full")); } // Show result of appending element ListBox_SetCurSel(hwndLB, ListBox_AddString(hwndLB, sz)); Sleep(2500); // Wait before appending another element } return 0; } ////////////////////////////////////////////////////////////////////////// DWORD WINAPI ServerThread(PVOID pvParam) { int nThreadNum = PtrToUlong(pvParam); HWND hwndLB = GetDlgItem(g_hwnd, IDC_SERVERS); while (1 != InterlockedCompareExchange(&g_fShutdown, 0, 0)) { TCHAR sz[1024]; CQueue::ELEMENT e; // Try to get an element from the queue if (g_q.Remove(&e, 5000)) { // Indicate which thread is processing it, which thread // sent it and which request we're processing StringCchPrintf(sz, _countof(sz), TEXT("%d: Processing %d:%d"), nThreadNum, e.m_nThreadNum, e.m_nRequestNum); // The server takes some time to process the request Sleep(2000 * e.m_nThreadNum); } else { // Couldn't get an element from the queue StringCchPrintf(sz, _countof(sz), TEXT("%d: (timeout)"), nThreadNum); } // Show result of processing element ListBox_SetCurSel(hwndLB, ListBox_AddString(hwndLB, sz)); } return 0; } ////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) { chSETDLGICONS(hwnd, IDI_QUEUE); g_hwnd = hwnd; // Used by client/server threads to show status DWORD dwThreadID; // Create the client threads for (int x = 0; x < 4; x++) g_hThreads[g_nNumThreads++] = chBEGINTHREADEX(NULL, 0, ClientThread, (PVOID)(INT_PTR)x, 0, &dwThreadID); // Create the server threads for (int x = 0; x < 2; x++) g_hThreads[g_nNumThreads++] = chBEGINTHREADEX(NULL, 0, ServerThread, (PVOID)(INT_PTR)x, 0, &dwThreadID); return TRUE; } ////////////////////////////////////////////////////////////////////////// void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; } } INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); } return FALSE; } ////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int) { DialogBox(hInstExe, MAKEINTRESOURCE(IDD_QUEUE), NULL, Dlg_Proc); InterlockedExchange(&g_fShutdown, TRUE); // Wait for all the threads to terminate & then cleanup WaitForMultipleObjects(g_nNumThreads, g_hThreads, TRUE, INFINITE); while (g_nNumThreads--) CloseHandle(g_hThreads[g_nNumThreads]); return 0; } //////////////////////////////// End of File //////////////////////////////////
9.7 线程同步速查表
9.8 其他线程同步函数
9.8.1 异步设备I/O
异步设备IO(asynchronous device I/O)允许线程开始读取操作或写入操作,但不必等待读取操作或写入操作完成。设备对象是是可同步的内核对象,可以调用WaitForSingleObject并传入句柄,套接字,通信端口等。
9.8.2 WaitForInputIdle函数
线程可以调用此函数将自己挂起WINUSERAPI DWORD WINAPI WaitForInputIdle( _In_ HANDLE hProcess, _In_ DWORD dwMilliseconds);
常用于等待子进程,父进程知道子进程已经初始化完毕的唯一方法,就是等待子进程,直到它不再处理任何输入为止。
当我们要强制在应用程序中输入一些按键的时候,也可以使用WaitForInputIdle。
当向目标进程发送一系列按键消息以后,调用WaitForInputIdle等待其处理完按键消息,然后再发送后续的按键消息。
9.8.3 MsgWaitForMultipleObjects(Ex)函数
WINUSERAPI DWORD WINAPI MsgWaitForMultipleObjects( _In_ DWORD nCount, _In_reads_opt_(nCount) CONST HANDLE *pHandles, _In_ BOOL fWaitAll, _In_ DWORD dwMilliseconds, _In_ DWORD dwWakeMask); WINUSERAPI DWORD WINAPI MsgWaitForMultipleObjectsEx( _In_ DWORD nCount, _In_reads_opt_(nCount) CONST HANDLE *pHandles, _In_ DWORD dwMilliseconds, _In_ DWORD dwWakeMask, _In_ DWORD dwFlags);
函数功能:阻塞时仍可以响应消息
MsgWaitForMultipleObjects()函数类似WaitForMultipleObjects(),
但它会在“对象被激发”或“消息到达队列”时被唤醒而返回。
MsgWaitForMultipleObjects()多接收一个参数,允许指定哪些消息是观察对象。
一个应用的例子 该函数同时等待对象,若有消息到底也返回。运行主线程处理消息后继续等待。
DWORD dwRet = 0; MSG msg; DWORD dwStartTime = GetTickCount(); while (TRUE) { //超时判断 5s dwRet = GetTickCount() - dwStartTime; if ((GetTickCount() - dwStartTime) > 10000) { AfxMessageBox(_T("获取数据超时,请检测设备网络连接!"), MB_OK | MB_ICONERROR); return NULL; } //wait for m_hThread to be over,and wait for //QS_ALLINPUT(Any message is in the queue) //dwRet = WaitForSingleObject(g_hRetEvent, INFINITE); dwRet = MsgWaitForMultipleObjects(1, &g_hRetEvent, FALSE, 100, QS_ALLINPUT); switch (dwRet) { case WAIT_OBJECT_0: //返回数据达到 break; //break the loop case WAIT_OBJECT_0 + 1: //界面消息 //get the message from Queue //and dispatch it to specific window if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } continue; case WAIT_TIMEOUT: //超时 continue; default: AfxMessageBox(_T("数据获取失败,未知错误!"), MB_OK | MB_ICONERROR); return NULL; break; // unexpected failure } break; }
9.8.4 WaitForDebugEvent 函数
调试器Attach到被调试程序以后,调用WaitForDebugEvent来等待调试事件。WINBASEAPI BOOL APIENTRY WaitForDebugEvent( _Out_ LPDEBUG_EVENT lpDebugEvent, _In_ DWORD dwMilliseconds );
9.8.5 SignalObjectAndWait函数
WINBASEAPI DWORD WINAPI SignalObjectAndWait( _In_ HANDLE hObjectToSignal, _In_ HANDLE hObjectToWaitOn, _In_ DWORD dwMilliseconds, _In_ BOOL bAlertable );使用一个原子操作来触发一个内核对象,并等待另一个内核对象。
hObjectToSignal必须是一个互斥量,信号量或事件。(其他任何对象将导致函数返回WAIT_FAILD)调用GetLastError返回ERROR_INVALID_HANDLE.
hObjectToWaitOn可以是互斥量,信号量,事件,进程,线程,作业,控制台输入变更通知,作业。等等
dwMilliseconds 函数最多花多长时间来等待。
bAlertable表示当线程处于等待状态的时候,是否能够堆添加到队列中的异步过程调用进行处理。
返回值:WAIT_OBJECT_0, WAIT_TIMEOUT, WAIT_FAILED, WAIT)ABANDONED, WAIT_IO_COMPLETION
配合PulseEvent使用。
SignalObjectAndWait释放一个对象,同时立即等待(原子方式)
能确保其100%能看见别的线程调用的PulseEvent。
9.8.6 使用等待链遍历API来检测死锁
Vista系统以上提供了等待链遍历(Wait Chain Traversal, WCT)API,这些函数可以让我们列出所有的锁,并检测进程内部,甚至是进程之间的死锁。LockCop示例程序
LockCop展示如何使用WCT函数来创建一个非常有用的工具。
等待链
一条等待链是一个序列,在这个序列中线程和同步对象交替出现,每个线程等待它后面的对象,而该对象却为等待链中更后面的线程所占用。
例如3212正在等待线程2260释放一个关键段,而线程2260正在等待线程3212释放另外一个关键段。这就是典型的死锁。
代码LockCop.cpp
/****************************************************************************** Module: LockCop.cpp Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre ******************************************************************************/ #include "..\CommonFiles\CmnHdr.h" /* See Appendix A. */ #include "..\CommonFiles\ToolHelp.h" #include "ChainParser.h" #include "resource.h" #include <windowsx.h> #include <tchar.h> #include <StrSafe.h> /////////////////////////////////////////////////////////////////////////////// // Global Variables HINSTANCE g_hInstance; HWND g_hDlg; #define DETAILS_CTRL GetDlgItem(g_hDlg, IDC_EDIT_DETAILS) ////////////////////////////////////////////////////////////////////////// // Adds a String to the "Details" edit control void AddText(PCTSTR pszFormat, ...) { va_list argList; va_start(argList, pszFormat); TCHAR sz[20 * 1024]; Edit_GetText(DETAILS_CTRL, sz, _countof(sz)); _vstprintf_s( _tcschr(sz, TEXT('\0')), _countof(sz) - _tcslen(sz), pszFormat, argList); Edit_SetText(DETAILS_CTRL, sz); va_end(argList); } ////////////////////////////////////////////////////////////////////////// void OnRefreshProcesses() { HWND hwndList = GetDlgItem(g_hDlg, IDC_COMBO_PROCESS); SetWindowRedraw(hwndList, FALSE); ComboBox_ResetContent(hwndList); CToolhelp thProcesses(TH32CS_SNAPPROCESS); PROCESSENTRY32 pe = { sizeof(pe) }; BOOL fOk = thProcesses.ProcessFirst(&pe); for (; fOk; fOk = thProcesses.ProcessNext(&pe)) { TCHAR sz[1024]; // Place the process name(without its path) & ID in the list PCTSTR pszExeFile = _tcschr(pe.szExeFile, TEXT('\\')); if (pszExeFile == NULL) { pszExeFile = pe.szExeFile; } else { pszExeFile++; // skip over the slash } StringCchPrintf(sz, _countof(sz), TEXT("%04u - %s"), pe.th32ProcessID, pszExeFile); int n = ComboBox_AddString(hwndList, sz); // Associate the process ID with the added item ComboBox_SetItemData(hwndList, n, pe.th32ProcessID); } ComboBox_SetCurSel(hwndList, 0); // Select the first entry // Simulate the user selecting this first item so that the // results pane shows something interesting FORWARD_WM_COMMAND(g_hDlg, IDC_COMBO_PROCESS, hwndList, CBN_SELCHANGE, SendMessage); SetWindowRedraw(hwndList, TRUE); InvalidateRect(hwndList, NULL, FALSE); } ////////////////////////////////////////////////////////////////////////// void OnUpdateLocks() { SetWindowText(DETAILS_CTRL, TEXT("")); // Clear the output box // GetCurrent process from the combo box HWND hwndCtl = GetDlgItem(g_hDlg, IDC_COMBO_PROCESS); DWORD dwSelection = ComboBox_GetCurSel(hwndCtl); DWORD PID = (DWORD)ComboBox_GetItemData(hwndCtl, dwSelection); AddText(TEXT("Thread in process %u\r\n"), PID); CChainParser parser(DETAILS_CTRL); parser.ParseThreads(PID); } ////////////////////////////////////////////////////////////////////////// void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDOK: case IDCANCEL: // User has clicked on the OK button // or dismissed the dialog with ESCAPE EndDialog(hwnd, id); break; case IDC_COMBO_PROCESS: if (codeNotify == CBN_SELCHANGE) { OnUpdateLocks(); } break; case IDC_BTN_REFRESH: OnRefreshProcesses(); break; case IDC_BTN_UPDATE: OnUpdateLocks(); break; } } ////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) { chSETDLGICONS(hwnd, IDI_LOCKCOP); // Keep track of the main dialog window handle g_hDlg = hwnd; // Have the results window use a fixed-pitch font SetWindowFont(GetDlgItem(hwnd, IDC_EDIT_DETAILS), GetStockFont(ANSI_FIXED_FONT), FALSE); // Fill up the process combo-box OnRefreshProcesses(); return TRUE; } ////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); } return FALSE; } ////////////////////////////////////////////////////////////////////////// int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); // Keep track of the module handle g_hInstance = hInstance; // Enabling the debug privilege allows the application to see // Information about service applications CToolhelp::EnablePrivilege(SE_DEBUG_NAME, TRUE); // Show main window DialogBox(hInstance, MAKEINTRESOURCE(IDD_LOCKCOP), NULL, Dlg_Proc); // Restore privileges // Even though it is not really important since the process is existing CToolhelp::EnablePrivilege(SE_DEBUG_NAME, FALSE); return 0; }
运行结果(检测例子BadLock)
LockCop不支持WaitForMultileObjects
相关文章推荐
- 《Windows核心编程 5th》读书笔记----第9章 用内核对象进行线程同步
- Windows核心编程 用内核对象进行线程同步
- Windows核心编程(八)用内核对象进行线程同步
- windows核心编程-用内核对象进行线程同步
- windows核心编程-9.用内核对象进行线程同步
- 《Windows核心编程》读书笔记八 用户模式下的内核同步
- <<Windows核心编程(第五版)>>第九章用内核对象进行线程同步:9.3事件内核对象
- Windows核心编程:(六)用内核对象进行线程同步
- 摘自windows核心编程之用内核对象进行线程同步
- Windows核心编程:用内核对象进行线程同步
- (摘自windows核心编程之用内核对象进行线程同步)
- 《Windows核心编程》——九 用内核对象进行线程同步
- (摘自windows核心编程之用内核对象进行线程同步)
- <<windows核心编程>>读书笔记---第9章 内核对象进行线程同步
- 内核模式下的线程同步——事件内核对象
- 【windows核心编程】 第八章 用户模式下的线程同步
- 《Windows核心编程》读书笔记三 内核对象
- 第9章 用内核对象进行线程同步(4)_死锁(DeadLock)及其他
- Windows核心编程笔记 用户模式下的线程同步