《Windows核心编程》读书笔记九 用内核模式进行线程同步

来源:互联网 发布:数值分析教材 知乎 编辑:程序博客网 时间:2024/05/19 06:46

第九章 用内核对象进行线程同步


本章内容

9.1 等待函数

9.2 等待成功所引起的副作用

9.3 事件内核对象

9.4 可等待的计时器内核对象

9.5 信号量内核对象

9.6 互斥量内核对象

9.7 线程同步对象速查表

9.8 其他的线程同步函数


用户模式下的线程同步高性能,但是存在一些局限。例如无法进行进程间线程的同步,Iterlocked系函数不会把线程切换到等待状态,

进入临界区无法设置最长等待时间等。


内核对象来进行线程同步,功能强大许多。但是唯一的缺点就是性能。


对于线程内核对象可能处于触发(signaled)和未触发(nosignaled)

进程内核对象,在创建时其内部有一个BOOL变量是FALSE, 单进程终止时该内核对象会变成TRUE 表示已经触发

但是这个过程是不可逆的。


以下列出可能处于未触发也可以处于触发状态的内核对象:

进程,线程,作业,文件以及控制台的标准输入流/输出流/错误流

事件,可等待的计时器, 信号量,互斥量


9.1 等待函数

等待函数使一个线程资源进入等待状态,直到指定的内核对象被触发为止。如果在调用一个等待函数时,响应的内核对象已经处于触发状态,那么线程是不会进入等待状态的。

WINBASEAPIDWORDWINAPIWaitForSingleObject(    _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的返回值表示为什么调用线程又能继续执行了。


以下函数可以等待多个内核对象的触发状态

WINBASEAPIDWORDWINAPIWaitForMultipleObjects(    _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_HANDLEWINAPICreateEventW(    _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_HANDLEWINAPICreateEventExW(    _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_HANDLEWINAPIOpenEventW(    _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.cppNotices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre******************************************************************************/#include "..\CommonFiles\CmnHdr.h"#include <windowsx.h>#include <tchar.h>#include "Resource.h"//////////////////////////////////////////////////////////////////////////#define BUFFERSIZ1024// This event is signaled when the client has a request for the serverHANDLEg_hevtRequestSubmitted;// This event is signaled when the server has a result for the clientHANDLEg_hevtResultReturned;// The buffer shared between the client and server threadsTCHARg_szSharedRequestAndResultBuffer[BUFFERSIZ];// The special value sent from the client that causes the// server thread to terminate cleanly.TCHARg_szServerShutdown[] = TEXT("Server Shutdown");// The server thread will check that the main dialog is no longer alive// When the shutdown message is received.HWNDg_hMainDlg;//////////////////////////////////////////////////////////////////////////// This is the code executed by the server threadDWORD WINAPI ServerThread(PVOID pvParam) {// Assume that the server thread is to run foreverBOOL fShutdown = FALSE;while (!fShutdown) {// Wait for the client to submit a requestWaitForSingleObject(g_hevtRequestSubmitted, INFINITE);// Check to see if the client wants the server to terminatefShutdown =(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 resultSetEvent(g_hevtResultReturned);}// The client wants us to shut down, exitreturn 0;}//////////////////////////////////////////////////////////////////////////BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {chSETDLGICONS(hwnd, IDI_HANDSHAKE);// Initialize the edit control with some test data requestEdit_SetText(GetDlgItem(hwnd, IDC_REQUEST), TEXT("Some test data"));// Store the main dialog window handleg_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 bufferEdit_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 resultSignalObjectAndWait(g_hevtRequestSubmitted, g_hevtResultReturned, INFINITE, false);// Let the user know the resultEdit_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 eventsg_hevtRequestSubmitted = CreateEvent(NULL, FALSE, FALSE, NULL);g_hevtResultReturned = CreateEvent(NULL, FALSE, FALSE, NULL);// Spawn the server threadDWORD dwThreadID;HANDLE hThreadServer = chBEGINTHREADEX(NULL, 0, ServerThread, NULL,0, &dwThreadID);// Execute the client thread's user interfaceDialogBox(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 everythingCloseHandle(hThreadServer);CloseHandle(g_hevtRequestSubmitted);CloseHandle(g_hevtResultReturned);// The client thread terminates with the whole processreturn 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_VARIABLEg_cvReturnResult;CONDITION_VARIABLEg_cvSubmitRequest;SRWLOCKg_srwLock;TCHARg_szSharedBuffer[BUFSIZ];TCHARg_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 SRWLockbShutdown =(_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 srwlockInitializeConditionVariable(&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);#elsegets(g_szSharedBuffer);#endifbShutdown = (_tcscmp(g_szSharedBuffer, g_szShutdown) == 0);if (!bShutdown) {AcquireSRWLockExclusive(&g_srwLock);WakeConditionVariable(&g_cvSubmitRequest); // signal the server threadSleepConditionVariableSRW(&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 UPCloseHandle(hThreadServer);system("pause");return 0;}



9.4 可等待的计时器内核对象


可等待的计时器是这样一种内核对象:他们会在某个指定的时间触发,或每间隔一段时间触发。

创建一个可等待的计时器

WINBASEAPI_Ret_maybenull_HANDLEWINAPICreateWaitableTimerW(    _In_opt_ LPSECURITY_ATTRIBUTES lpTimerAttributes,    _In_     BOOL bManualReset,    _In_opt_ LPCWSTR lpTimerName    );

打开一个可等待的计时器

WINBASEAPI_Ret_maybenull_HANDLEWINAPIOpenWaitableTimerW(    _In_ DWORD dwDesiredAccess,    _In_ BOOL bInheritHandle,    _In_ LPCWSTR lpTimerName    );


bManualReset表示要创建的是手动重置还是自动重置的计时器。

手动重置,等待该计时器的所有线程都变成可调度状态

自动重置,只有一个等待该计时器的线程会变成可调度状态


调用SetWaitableTimer设置计时器,能让其触发

WINBASEAPIBOOLWINAPISetWaitableTimer(    _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.HANDLEhTimer;SYSTEMTIMEst;FILETIMEftLocal, ftUTC;LARGE_INTEGERliUTC;// 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;// Yearst.wMonth= 1;// Januaryst.wDayOfWeek= 0;// Ignoredst.wDay= 1;// The first of the monthst.wHour= 13;// 1PMst.wMinute= 0;// 0 minutes into the hourst.wSecond= 0;// 0 seconds into the minutest.wMilliseconds= 0;// 0 milliseconds into the secondSystemTimeToFileTime(&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.HANDLEhTimer;LARGE_INTEGERli;// 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

WINBASEAPIBOOLWINAPICancelWaitableTimer(    _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.HANDLEhTimer;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_INTEGERli;// 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 Threadunsigned 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) {FILETIMEftUTC, ftLocal;SYSTEMTIMEst;TCHARszBuf[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_HANDLEWINAPICreateSemaphoreW(    _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_HANDLEWINAPIOpenSemaphoreW(    _In_ DWORD dwDesiredAccess,    _In_ BOOL bInheritHandle,    _In_ LPCWSTR lpName    );

dwDesiredAccess参数指定访问权限


线程通过调用ReleaseSemahore来递增信号量的当前资源计数:

WINBASEAPIBOOLWINAPIReleaseSemaphore(    _In_ HANDLE hSemaphore,    _In_ LONG lReleaseCount,    _Out_opt_ LPLONG lpPreviousCount    );

lReleaseCount 的值会加到信号量当前资源计数上。


9.6 互斥量内核对象

互斥量(mutex)内核对象用来确保一个线程独占一个资源的访问。

互斥量与临界区的行为完全相同。(内部保护递归计数)


互斥量的规则:

1)如果线程ID为0,那么互斥量不为任何线程所占用,它处于触发状态

2)如果线程ID为非零值,那么一个线程已经占用了该互斥量,它处与未触发状态。

3)与所有其他内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规的规则。(递归计数器的存在,运行同一个线程ID多次进入)


创建互斥量

WINBASEAPI_Ret_maybenull_HANDLEWINAPICreateMutexW(    _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_HANDLEWINAPICreateMutexExW(    _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_HANDLEWINAPIOpenMutexW(    _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.cppNotices: 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:PELEMENTm_pElements;// Array of elements to be processedintm_nMaxElements;// Maximum # of elements in the arrayHANDLEm_h[2];// Mutex & semaphore handlesHANDLE&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 queueLONG lPrevCount;fOk = ReleaseSemaphore(m_hsemNumElements, 1, &lPrevCount);if (fOk) {// The queue is not full, append the new elementm_pElements[lPrevCount] = *pElement;}else {// The queue is full, set the error code and return failureSetLastError(ERROR_DATABASE_FULL);}// Allow other threads to access the queueReleaseMutex(m_hmtxQ);}else {// Timeout, set error code and return failureSetLastError(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 downMoveMemory(&m_pElements[0], &m_pElements[1],sizeof(ELEMENT) * (m_nMaxElements - 1));// Allow other threads to access the queueReleaseMutex(m_hmtxQ);}else {// Timeout, set error code and return failureSetLastError(ERROR_TIMEOUT);}return fOk;// Call GetLastError for more info}//////////////////////////////////////////////////////////////////////////CQueue g_q(10);// The shared queuevolatile LONG g_fShutdown = FALSE;// Signals client/server threads to dieHWND g_hwnd;// How client/server threads give status// Handles to all client/server threads & number of client/server threadsHANDLEg_hThreads[MAXIMUM_WAIT_OBJECTS];intg_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 elementnRequestNum++;TCHAR sz[1024];CQueue::ELEMENT e = { nThreadNum, nRequestNum };// Try to put an element on the queueif (g_q.Append(&e, 200)) {// Indicate which thread sent it and which requestStringCchPrintf(sz, _countof(sz), TEXT("Sending %d:%d"),nThreadNum, nRequestNum);}else {// Couldn't put an element on the queueStringCchPrintf(sz, _countof(sz), TEXT("Sending %d:%d (%s)"),nThreadNum, nRequestNum, (GetLastError() == ERROR_TIMEOUT)? TEXT("timeout") : TEXT("full"));}// Show result of appending elementListBox_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 queueif (g_q.Remove(&e, 5000)) {// Indicate which thread is processing it, which thread// sent it and which request we're processingStringCchPrintf(sz, _countof(sz), TEXT("%d: Processing %d:%d"),nThreadNum, e.m_nThreadNum, e.m_nRequestNum);// The server takes some time to process the requestSleep(2000 * e.m_nThreadNum);}else {// Couldn't get an element from the queueStringCchPrintf(sz, _countof(sz), TEXT("%d: (timeout)"), nThreadNum);}// Show result of processing elementListBox_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 statusDWORD dwThreadID;// Create the client threadsfor (int x = 0; x < 4; x++)g_hThreads[g_nNumThreads++] =chBEGINTHREADEX(NULL, 0, ClientThread, (PVOID)(INT_PTR)x,0, &dwThreadID);// Create the server threadsfor (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 cleanupWaitForMultipleObjects(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函数

线程可以调用此函数将自己挂起

WINUSERAPIDWORDWINAPIWaitForInputIdle(    _In_ HANDLE hProcess,    _In_ DWORD dwMilliseconds);

常用于等待子进程,父进程知道子进程已经初始化完毕的唯一方法,就是等待子进程,直到它不再处理任何输入为止。


当我们要强制在应用程序中输入一些按键的时候,也可以使用WaitForInputIdle。

当向目标进程发送一系列按键消息以后,调用WaitForInputIdle等待其处理完按键消息,然后再发送后续的按键消息。


9.8.3 MsgWaitForMultipleObjects(Ex)函数

WINUSERAPIDWORDWINAPIMsgWaitForMultipleObjects(    _In_ DWORD nCount,    _In_reads_opt_(nCount) CONST HANDLE *pHandles,    _In_ BOOL fWaitAll,    _In_ DWORD dwMilliseconds,    _In_ DWORD dwWakeMask);WINUSERAPIDWORDWINAPIMsgWaitForMultipleObjectsEx(    _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来等待调试事件。

WINBASEAPIBOOLAPIENTRYWaitForDebugEvent(    _Out_ LPDEBUG_EVENT lpDebugEvent,    _In_ DWORD dwMilliseconds    );

9.8.5 SignalObjectAndWait函数

WINBASEAPIDWORDWINAPISignalObjectAndWait(    _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.cppNotices: 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 VariablesHINSTANCEg_hInstance;HWNDg_hDlg;#define DETAILS_CTRLGetDlgItem(g_hDlg, IDC_EDIT_DETAILS)//////////////////////////////////////////////////////////////////////////// Adds a String to the "Details" edit controlvoid 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 listPCTSTR 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 itemComboBox_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 interestingFORWARD_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 boxHWND 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 ESCAPEEndDialog(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 handleg_hDlg = hwnd;// Have the results window use a fixed-pitch fontSetWindowFont(GetDlgItem(hwnd, IDC_EDIT_DETAILS),GetStockFont(ANSI_FIXED_FONT), FALSE);// Fill up the process combo-boxOnRefreshProcesses();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 handleg_hInstance = hInstance;// Enabling the debug privilege allows the application to see// Information about service applicationsCToolhelp::EnablePrivilege(SE_DEBUG_NAME, TRUE);// Show main windowDialogBox(hInstance, MAKEINTRESOURCE(IDD_LOCKCOP), NULL, Dlg_Proc);// Restore privileges// Even though it is not really important since the process is existingCToolhelp::EnablePrivilege(SE_DEBUG_NAME, FALSE);return 0;}



运行结果(检测例子BadLock)




LockCop不支持WaitForMultileObjects


阅读全文
0 0
原创粉丝点击