Windows 核心编程 学习笔记 (第四部分)

目录

19. 用户态的线程同步方式

  •     互锁函数
  •     循环锁
  •     CRITICAL_SECTION 临界区

20. 内核态的线程同步方式

  •     内核对象用于线程同步
  •     等待函数
  •     Event – 事件
  •     WaitableTimer – 等待定时器
  •     Semaphore – 信号量
  •     Mutex – 互斥对象
  •     其他线程同步函数

21. 一个互斥写和共享读锁的实现例子

19. 用户态的线程同步方式

(1) 互锁函数 – InterLocked 函数族

常用的互锁函数有InterLockedAdd, InterLockedExchange,InterLockedExchangeAdd,InterLockedCompareExchange等

[cpp]//原子操作:加法 c <- a+b
LONG __cdecl InterlockedAdd( //返回 和
__inout  LONG volatile *Addend, //指向被加数的指针
__in     LONG Value             //加数
);

//原子操作:交换  a <- b
LONG __cdecl InterlockedExchange( //返回 Target交换之前的值
__inout  LONG volatile *Target, //指向要交换的变量的指针
__in     LONG Value //用来交换的值
);

//原子操作:对一个变量做加法 a += b
LONG __cdecl InterlockedExchangeAdd( //返回加之前的值
__inout  LONG volatile *Addend,  //指向要做加法的变量的指针
__in     LONG Value              //叠加的值
);

//原子操作:先比较,如果Destination==Comparand,则将Exchange赋值给Destination,否则不赋值
LONG __cdecl InterlockedCompareExchange(  //返回Destination的原始值
__inout  LONG volatile *Destination,
__in     LONG Exchange,
__in     LONG Comparand
);[/cpp]

互锁函数运行速度很快,通常只需几个CPU周期(通常小于50)的执行时间,并且不会从用户方式转化为内核方式。

(2) 循环锁

[cpp]BOOL g_fResourceInUse = FALSE; //全局标识,用于互斥访问

void Fun(){
//循环等待,直到标识的前一状态是FALSE
while (InterlockedExchange(&g_fResourceInUse,TRUE) == TRUE){
Sleep(0); //
}

//…访问互斥资源

//标识置为 FALSE
InterlockedExchange(&g_fResourceInUse,FALSE);
}[/cpp]

缺点:浪费CPU时间。CPU会不断的执行循环(即便使用Sleep,也会存在浪费,因为Sleep(0) 实际是只是放弃当前的CPU时间片,线程依然是可调度的)。可以通过延长Sleep的时间来减少CPU时间的浪费。

应该避免在单个CPU的计算机上使用循环锁。在多处理器的计算机上可以使用,因为此时一个线程循环执行时,另一个线程可以在另一个CPU上运行。

(3) CRITICAL_SECTION 临界区 (关键代码段)

[参考 C++线程同步总结(笔记)]

优点:内部使用了互锁函数,所以运行效率高。

缺点:无法对多个进程中的线程进程同步

[1] 定义临界区

CRITICAL_SECTION 结构体的定义在WinNT.h中

[cpp]typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;

//
//  The following three fields control entering and exiting the critical
//  section for the resource
//

LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread;        // from the thread’s ClientId->UniqueThread
HANDLE LockSemaphore;
ULONG_PTR SpinCount;        // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;[/cpp]

我们不需要关心其内部的成员变量。只要会用CRITICAL_SECTION 即可。

[cpp]CRITICAL_SECTION cs;   //[1]定义临界区结构体[/cpp]

[2] 初始化临界区 – InitializeCritialSection

[cpp]void WINAPI InitializeCriticalSection(            //初始化临界区
__out  LPCRITICAL_SECTION lpCriticalSection  //指向临界区变量的指针
);[/cpp]

如果线程进入未初始化的临界区,其结果是未定义的。

[cpp]InitializeCriticalSection(&cs); //[2]初始化临界区[/cpp]

还可以使用InitializeCriticalSectionAndSpinCount 函数初始化一个自旋的临界区。所谓自旋,指当一个线程试图进入一个已锁的临界区,线程会进入一个循环,检测锁是否被释放,如果锁没有被释放,则线程进入sleep.(自旋就是循环检测,如果已锁,则sleep一下,醒来继续检测)

[cpp]BOOL WINAPI InitializeCriticalSectionAndSpinCount(
__out  LPCRITICAL_SECTION lpCriticalSection,  //临界区
__in   DWORD dwSpinCount                      //自旋次数,即循环的次数
);[/cpp]

在单处理器系统上,自旋次数参数会被忽略且设置为0。在多处理器系统上,如果临界区已锁,线程会自旋dwSpinCount次,如果自旋dwSpinCount次后锁仍为释放,则进入等待(wait)。[sleep会自动唤醒,wait需等待其它线程唤醒]

[3] 进入临界区 – EnterCriticalSection

[cpp]void WINAPI EnterCriticalSection(                   //进入临界区:等待(堵塞),直到获取锁
_Inout_  LPCRITICAL_SECTION lpCriticalSection
);[/cpp]

进入等待状态的线程不消耗CPU。系统会记住该线程想要访问该资源且自动更新CRITICAL_SECTION的成员变量,一旦目前访问该资源的线程调用了LeaveCriticalSection,等待的线程就切换到可调度状态。

还可以使用TryEnterCriticalSection 函数,无论临界区是否已锁,都会立即返回。

[cpp]BOOL WINAPI TryEnterCriticalSection(              //如果当前线程成功进入临界区或已经进入临界区,返回非0值。
_Inout_  LPCRITICAL_SECTION lpCriticalSection //如果临界区被其他线程占用,则返回0
);[/cpp]

[4] 退出临界区 – LeaveCriticalSection

[cpp]void WINAPI LeaveCriticalSection(
_Inout_  LPCRITICAL_SECTION lpCriticalSection
);[/cpp]

一个线程可以多次EnterCriticalSection 而不会死锁,但一个Enter必须对应一个Leave。

[5] 删除临界区 – DeleteCriticalSection

[cpp]void WINAPI DeleteCriticalSection(
_Inout_  LPCRITICAL_SECTION lpCriticalSection
);[/cpp]

—————————————————————————————————————————————

临界区使用的技巧

-> 每个共享资源使用一个CRITICAL_SECTION

-> 同时Enter多个临界区时,注意Enter和Leave的顺序,避免形成死锁。如果两个线程同时执行下面的代码,会造成死锁。

[cpp]EnterCriticalSection(&cs1);
EnterCriticalSection(&cs2);
//…
LeaveCriticalSection(&cs1);  //会产生死锁。应该先Leave cs2,再Leave cs1.
LeaveCriticalSection(&cs2);[/cpp]

-> 不要长时间运行临界区。临界区访问越短越好。

20. 内核态的线程同步方式

(1) 内核对象用于线程同步

Windows中的一些内核对象拥有已通知(TRUE)和未通知(FALSE) 两种状态(即有信号和无信号两种状态),可以用于线程同步。每个对象的状态切换都有自己的一套规则。

这些内核对象包括:进程、线程、作业、文件、控制台输入、文件修改通知、可等待定时器、事件、Semaphore、Mutex。

以进程为例:进程总是在未通知状态中创建,正在运行的进程对象处于未通知状态,进程终止时就变味已通知状态。

所以可以使用WaitForSingleObject 等待进程的句柄来判断进程是否终止。

其中:可等待定时器、Event、Semaphore、Mutex 是Windows提供的专门用于线程同步的内核对象。

当线程等待的对象处于未通知状态,则线程不可调度的,当等待的对象变为已通知状态,则线程切换到可调度状态。

(2) 等待函数

等待函数使线程自愿进入等待状态,直到一个特定的内核对象变为已通知状态。

[1] WaitForSingleObject – 等待一个内核对象

[cpp]DWORD WINAPI WaitForSingleObject( //等待一个内核对象变为已通知状态
_In_  HANDLE hHandle,      //等待的内核对象句柄
_In_  DWORD dwMilliseconds //等待时间(单位ms),可以传INFINITE表示一直等待,直到等待对象变为已通知
);</pre>
<pre>DWORD dwRet = WaitForSingleObject(hThread,5000);

switch(dwRet){
case WAIT_OBJECT_0:
//等待内核对象变为已通知。如线程已结束
break;
case WAIT_TIMEOUT:
//等待超时
break;
case WAIT_FAILED:
//函数调用失败(如INVALID_HANDLE)。使用GetLastError获取具体信息.
break;
}[/cpp]

[2] WaitForMultipleObjects – 等待多个内核对象

[cpp]DWORD WINAPI WaitForMultipleObjects( //等待多个内核对象
_In_  DWORD nCount,           //等待的内核对象个数。最大为MAXIMUM_WAIT_OBJECTS,且不能为0
_In_  const HANDLE *lpHandles,//等待的内核对象数组。
_In_  BOOL <b>bWaitAll</b>,//TRUE:所有等待的内核对象都为已通知状态才返回。FALSE:只要有一个内核对象为已通知状态就返回
_In_  DWORD dwMilliseconds    //等待时间(单位ms)
);</pre>
<pre>HANDLE h[2];
h[0] = hThread1;
h[1] = hThread2;

DWORD dwRet = WaitForMultipleObjects(2,h,FALSE,5000);
switch(dwRet){
case WAIT_OBJECT_0:
//hThread1 为已通知状态。(即Thread1线程结束)
break;
case WAIT_OBJECT_0 + 1:
//hThread2 为已通知状态。(即Thread2线程结束)
break;
case WAIT_TIMEOUT:
//等待超时
break;
case WAIT_FAILED:
//函数调用失败(如INVALID_HANDLE)。使用GetLastError获取具体信息.
break;
case WAIT_ABANDONED_0 + 1:
//hThread2 内核对象被遗弃
break;
}[/cpp]

[3] 成功等待的副作用

对于某些内核对象,成功的调用了WaitForSingleObject和WaitForMultipleObject时会改变对象的状态,这就是成功等待的副作用。

不同的内核对象的成功等待的副作用不一样。如自动重置事件(AutoResetEvent)对象的成功等待的副作用是 即WaitFor函数返回前,该对象会被置为未通知状态。有些内核对象如线程、进程成功等待也不会有副作用

等待不成功绝不会有副作用。

-> WaitForMultipleObject 是以原子方式运行的,可以避免死锁的产生。如两个线程同时运行下面的代码:

[cpp]HANDLE h[2];
h[0] = hAutoResetEvent1; //初始为未通知状态
h[1] = hAutoResetEvent2; //初始为未通知状态
WaitForMultipleObjects(2,h,TRUE,INFINITE);[/cpp]

初始时两个对象都为未通知状态,两个线程都进入等待。

然后,当hAutoResetEvent1变为已通知时,两个线程都能发现该状态改变,但是都无法唤醒,因为hAutoResetEvent2仍处于未通知,所有两个线程都没有成功等待,不会对hAutoResetEvent1对象产生副作用(即没有将它置为未通知状态)

然后,当红AutoResetEvent2变为已通知时,两个线程中的一个线程发现该变化,两个对象都变为已通知状态,成功等待,由于副作用的存在,会把两个对象会置为未通知状态,该线程变为可调度线程。此时,另一个线程将继续等待,直到前一个线程的WaitForMultpleObject函数返回,两个对象重新变会已通知状态。

这样就避免了两个线程的死锁。

如果WaitForMultipleObject不是原子操作的,那么可能发生一个线程将hAutoResetEvent1置为未通知状态,另一个线程将hAutoResetEvent2置为未通知状态,两个线程就会互相等待,形成死锁。

【至于两个线程同时发现内核对象已通知状态后,系统究竟会执行哪个,这个是不确定的。Microsoft只说使用的算法是公平的,但具体的算法不知道。实际操作是FIFO算法,等待时间最长的线程获得该对象,但也不是绝对】

(3) Event – 事件内核对象

Event是最基本的内核对象。包括一个使用计数(所有内核对象都有)、一个指明该Event是自动重置事件还是手动重置事件的BOOL值、一个指明该Event处在已通知状态还是未通知状态的BOOL值。

-> 自动重置事件:事件被设置为已通知(有信号)状态后,会唤醒一个等待线程,然后自动恢复为未通知(无信号)状态。

-> 手动重置事件:事件被设置为已通知状态后,事件会保持已通知状态,直到使用ResetEvent重置为未通知状态。当事件处于已通知状态时,所有等待事件的线程都会等到事件,并被唤醒为可调度状态。

可见两者的区别在于:自动重置事件一次只能唤醒一个等待线程,手动重置事件会唤醒所有的等待线程

[自动重置事件会有成功等待副作用(如上),手动重置事件没有成功等待副作用]

[1] 创建事件对象 – CreateEvent

[cpp]HANDLE WINAPI CreateEvent( //创建事件对象,返回其句柄
_In_opt_  LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性,可以为NULL
_In_      BOOL bManualReset, //TRUE表示手动重置事件,FALSE表示自动重置事件
_In_      BOOL bInitialState,//初始状态,TRUE表示已通知,FALSE表示未通知
_In_opt_  LPCTSTR lpName     //内核对象名称,可以为NULL
);[/cpp]

[2] 设置事件对象状态 – SetEvetn,ResetEvent

[cpp]BOOL SetEvent( __in HANDLE hEvent );  //事件hEvent置为已通知状态。成功返回非0,否则返回0
BOOL ResetEvent( __in HANDLE hEvent );//事件hEvent置为未通知状态。成功返回非0,否则返回0[/cpp]

(4) WaitableTimer – 等待定时器内核对象

等待定时器对象是一个在某个时间或每隔规定时间间隔变为有信号(已通知)状态的内核对象。

-> 自动重置等待定时器(又称同步定时器):一个等待线程等到之后,会置为未通知状态。只能唤醒一个等待线程。

-> 手动重置等待定时器:会保持已通知状态,直到SetWaitableTimer。可以唤醒所有的等待线程。

[1] CreateWaitableTimer – 创建等待定时器内核对象

[cpp]HANDLE WINAPI CreateWaitableTimer(  //创建等待定时器内核对象,初始为未通知状态
_In_opt_  LPSECURITY_ATTRIBUTES lpTimerAttributes,
_In_      BOOL bManualReset,  //TRUE:手动重置等待定时器,FALSE:自动重置等待定时器
_In_opt_  LPCTSTR lpTimerName
);[/cpp]

[2] SetWaitableTimer – 设置等待定时器内核对象

刚创建的定时器对象是未通知状态的,必须使用SetWaitableTimer设置其合适变为已通知状态

[cpp]BOOL WINAPI SetWaitableTimer(   _In_      HANDLE hTimer,                  //等待定时器对象句柄
_In_      const LARGE_INTEGER *pDueTime,  //第一次报时的时间,和FILETIME结构体格式相同,UTC时间。正值表示绝对时间,负值表示相对时间
_In_      LONG lPeriod,                   //每隔多少毫秒报时一次。0表示只报时一次。
_In_opt_  PTIMERAPCROUTINE pfnCompletionRoutine, //APC函数。(相当于回调函数)
_In_opt_  LPVOID lpArgToCompletionRoutine, //传递给APC函数的参数
_In_      BOOL fResume //用于支持暂停和恢复的计算机。通常设为FALSE。
);[/cpp]

-> pDueTime。正值表示一个绝对时间,负值表示相对时间,即SetWaitableTimer函数调用后多久报时。

->fResume。TRUE表示当定时器报时时,它是计算机摆脱暂停状态(如果它处在暂停状态的话),并唤醒等待定时器的线程。FALSE表示当定时器报时时,如果计算机处于暂停状态,它唤醒的线程必须等到计算机恢复运行后才能被调度。

绝对时间报时的例子

[cpp]#include <windows.h>

HANDLE hTimer;  //定时器

//线程回调函数
DWORD WINAPI ThreadProc(LPVOID lpParam){
int nCnt = 0;
while(TRUE){
DWORD dwRet = WaitForSingleObject(hTimer,INFINITE);
if(dwRet == WAIT_OBJECT_0){
MessageBox(NULL,_T("报时"),_T("提示"),MB_OK);
nCnt++;
}
if(nCnt >= 10 ) break; //10次后结束
}

return 0;
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine,int nCmdShow)
{

SYSTEMTIME st;
FILETIME ftLocal,ftUTC;
LARGE_INTEGER liUTC;

//创建自动重置的等待定时器
hTimer = CreateWaitableTimer(NULL,FALSE,NULL);

//设定定时器时间(绝对时间) 2014-5-7 10:23:0
st.wYear = 2014;
st.wMonth = 5;
st.wDayOfWeek = 0; //忽略
st.wDay = 7;
st.wHour = 10;
st.wMinute = 23;
st.wSecond = 0;
st.wMilliseconds = 0;

SystemTimeToFileTime(&st,&ftLocal);      //系统时间转化为本地FileTime
LocalFileTimeToFileTime(&ftLocal,&ftUTC);//本地FileTime转换为UTC FileTime

liUTC.LowPart = ftUTC.dwLowDateTime;
liUTC.HighPart = ftUTC.dwHighDateTime;

//设置等待定时器的时间
SetWaitableTimer(hTimer,&liUTC,10*1000,NULL,NULL,FALSE);//每隔10s报时一次

//启动线程
HANDLE hThread;
hThread = CreateThread(NULL,0,ThreadProc,NULL,0,NULL);

//等待线程结束
WaitForSingleObject(hThread,INFINITE);//等待线程结束

CloseHandle(hThread);
CloseHandle(hTimer);
return 0;
}[/cpp]

相对时间报时的例子:与绝对时间类似,只是时间设置不同

[cpp]LARGE_INTEGER li;

//创建自动重置的等待定时器
hTimer = CreateWaitableTimer(NULL,FALSE,NULL);

//设定定时器时间(相对时间) 5s之后报时
const int nTimerUnitsPerSecond = 10000000; //每秒包含的 100ns 个数
li.QuadPart = -(5 * nTimerUnitsPerSecond);  //5s

//设置等待定时器的时间
SetWaitableTimer(hTimer,&li,10*1000,NULL,NULL,FALSE);//每隔10s报时一次[/cpp]

[3] CancelWaitableTimer – 取消定时器

不再报时

[4] WaitableTimer 和 SetTimer比较

SetTimer 创建的定时器称为用户定时器。

-> 需要使用窗口UI结构,产生WM_TIMER消息,消息只能被调用线程获取,即只有一个线程能得到通知。WM_TIMER是优先级最低的消息,只有消息队列中没有其他消息时,才会获取该消息。

WaitableTimer

-> 内核对象,可供多个线程共享,且是安全的。可以唤醒多个等待线程。

(5) Semaphore – 信号量内核对象

信号量用于对资源进行计数,可以实现资源的互斥和共享,能够控制同时访问资源的线程数量。

信号量有两个计数:一个是最大的资源数量,一个是当前资源数量。

-> 当前资源数量 >0,则发出信号

-> 当前资源数量 =0,则不发出信号

-> 当前资源数量不会 < 0

-> 当前资源数量不能超过最大资源数量

-> Wait函数(WaitForSingleObject,WaitForMultipleObject)成功等待,会自动将信号量的当前资源数量 -1

[1] CreateSemaphore – 创建信号量

[cpp]HANDLE WINAPI CreateSemaphore(  //创建信号量
_In_opt_  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, //
_In_      LONG lInitialCount, //初始资源数量
_In_      LONG lMaximumCount, //最大资源数量
_In_opt_  LPCTSTR lpName //
);[/cpp]

[2] ReleaseSemaphore – 释放信号量

[cpp]BOOL WINAPI ReleaseSemaphore(   //释放信号量
_In_       HANDLE hSemaphore,
_In_       LONG lReleaseCount,    //释放资源的数量
_Out_opt_  LPLONG lpPreviousCount //释放前的资源数量
);[/cpp]

使用Wait函数是信号量当前资源数量-1。使用多次Wait函数可以递减当前资源数量多次。

ReleaseSemaphore可以是当前资源数量增加lReleaseCount个,如果当前资源数量+lReleaseCount > 最大资源数量,则函数不会修改当前资源数量的值,并且返回FALSE。

[3] Semaphore的两个典型应用场景

-> 应用程序使用信号量来限制同时使用资源的线程数量。线程在使用资源前使用Wait函数等待资源可用,Wait成功等待后,会递减当前资源的数量,线程在使用完资源后,使用ReleaseSemaphore递增当前资源数量。

-> 应用程序在初始化时使用信号量。应用程序创建一个初始资源数量为0的信号量,这样所有其他要访问资源的线程都会堵塞等待,当应用程序初始化完成后,使用ReleaseSemaphore释放最大资源数量个资源,允许等待的线程访问资源。

(6) Mutex- 互斥对象内核对象

互斥对象能够保证对单个资源的互斥访问。属于最常用的内核对象之一。

包括:一个使用数量(同其他内核对象一样)、一个线程ID(指明当前拥有Mutex的线程,0表示没有线程拥有该Mutex)、一个递归计数器(同一个线程拥有Mutex的次数)。

-> 线程ID为0,表示互斥对象不被任何线程拥有,互斥对象处于已通知状态

-> 线程ID非0,表示互斥对象已被线程拥有,互斥对象处于未通知状态。

如果当前拥有Mutex的线程再次等待该Mutex,系统判断线程ID与Mutex的线程ID相同,系统会使该线程继续保持可调度状态,Mutex的递归计数器值递增。ReleaseMutex使递归计数器递减,如果递归计数器值变为0,则Mutex的线程ID为被置为0,Mutex处于未通知状态。

[1] CreateMutex – 创建互斥对象

[cpp]HANDLE WINAPI CreateMutex(  //创建互斥对象
_In_opt_  LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_      BOOL bInitialOwner, //当前线程是否是互斥对象的初始拥有者,通常设为FALSE:不拥有
_In_opt_  LPCTSTR lpName
);[/cpp]

[2] ReleaseMutex – 释放互斥对象

[cpp]BOOL WINAPI ReleaseMutex(
_In_  HANDLE hMutex
);[/cpp]

互斥对象有个“线程所有权”的概念,如果不是拥有Mutex的线程调用ReleaseMutex释放,操作会失败。

如果拥有Mutex的线程直到终止都没有释放Mutex,那么系统会将Mutex视为已经被放弃的,等待的线程会得到一个WAIT_ABANDONED的返回值,表明拥有Mutex的线程直到终止都未释放Mutex,此时共享资源的额情况是未知的,但Mutex是可用的,获得通知的线程决定自身后续怎么做。

[cpp]case WAIT_ABANDONED:
//拥有等待的内核对象的线程直到终止都没有释放内核对象.内核对象会自动被调用线程拥有,初始状态为未通知。[/cpp]

(7) 其他的线程同步函数

[1] WaitForInputIdle – 等待进程,直到其处于空闲状态

[cpp]DWORD WINAPI WaitForInputIdle(  //等待进程hProcess,直到该进程在<b>创建应用程序的第一个窗口的线程</b>中已经没有尚未处理的输入为止。
_In_  HANDLE hProcess,      //即该进程第一个创建窗口的线程已初始化完成,并处于等待用户输入状态。
_In_  DWORD dwMilliseconds
);[/cpp]

如父进程创建了子进程后,想要获取子进程的创建的窗口的句柄,就可以通过调用WaitForInputIdle等待子进程窗口的初始化完成。

[cpp]#include <windows.h>

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine,int nCmdShow)
{
STARTUPINFO si = {sizeof(si)};
PROCESS_INFORMATION pi;
//CreateProcess(_T("C:\\Windows\\System32\\cmd.exe"),NULL,NULL,NULL,FALSE,NULL,NULL,NULL,&si,&pi);         //总是返回WAIT_FAILED,因为cmd没有窗口
CreateProcess(_T("C:\\Program Files\\TTPlayer\\TTPlayer.exe"),NULL,NULL,NULL,FALSE,NULL,NULL,NULL,&si,&pi);//正常,返回0

DWORD dwRet = WaitForInputIdle(pi.hProcess,INFINITE);
switch(dwRet){
case 0:
MessageBox(NULL,_T("子进程初始化成功"),_T("提示"),MB_OK);
break;
case WAIT_TIMEOUT:
MessageBox(NULL,_T("子进程初始化超时"),_T("提示"),MB_OK);
break;
case WAIT_FAILED:
MessageBox(NULL,_T("子进程初始化失败"),_T("提示"),MB_OK);
break;
}

WaitForSingleObject(pi.hProcess,INFINITE);

return 0;
}[/cpp]

[2] MsgWaitForMultipleObject – 等待内核对象 和 消息

[cpp]DWORD WINAPI MsgWaitForMultipleObjects(  //等待内核对象和消息(类似于WaitForMultipleObjects,不过可以等待消息)
_In_  DWORD nCount, //等待句柄的个数
_In_  const HANDLE *pHandles, //等待句柄
_In_  BOOL bWaitAll, //是否等到所有句柄都为已通知状态才返回
_In_  DWORD dwMilliseconds, //
_In_  DWORD dwWakeMask //等待的消息类型,QS_ALLEVENTS,QS_ALLINPUT,QS_HOTKEY
);[/cpp]

21. 一个互斥写和共享读锁的实现例子

[cpp]#include <stdio.h>
#include <Windows.h>
#include <process.h>
#include <time.h>

#define THREAD_COUNT 10

typedef unsigned (__stdcall *PTHREAD_START) (void *);

//控制一个线程写,多个线程读
class SingleWriteMultipleReadGuard{
public:
SingleWriteMultipleReadGuard();
~SingleWriteMultipleReadGuard();

void WaitToRead();  //调用本函数以获取 共享读锁
void WaitToWrite(); //调用本函数以获取 排他写锁
void Done(); //释放资源
private:
CRITICAL_SECTION m_cs; //控制成员函数操作的原子性
int m_nActive; //共享资源的当前状态。0表示没有线程在访问,>0 表示当前读取资源的线程数量。
//负数表示当前有一个线程正在写资源,唯一有效负值是-1
int m_nWaitingReaders; //想要读资源的线程数量。初始为0,当m_nActive为-1时,调用一次WaitToRead,该值+1
int m_nWaitingWriters; //想要写资源的线程数量。初始为0,当m_nActive不为0时,调用一次WaitToWrite,该值+1
HANDLE m_hsemReaders; //读锁的信号量
HANDLE m_hsemWriters; //写锁的信号量
};

//构造函数
SingleWriteMultipleReadGuard::SingleWriteMultipleReadGuard(){
m_nWaitingReaders = m_nWaitingWriters = m_nActive = 0; //初始化等待读写的线程和当前读写线程的数量都为0
m_hsemReaders = CreateSemaphore(NULL,MAXLONG,MAXLONG,NULL); //读锁的信号量,初始资源数为最大,可控资源为最大
m_hsemWriters = CreateSemaphore(NULL,1,MAXLONG,NULL); //写锁的信号量,初始资源数为1,可控资源为最大
InitializeCriticalSection(&m_cs);
}

//析构函数
SingleWriteMultipleReadGuard::~SingleWriteMultipleReadGuard(){
DeleteCriticalSection(&m_cs);
CloseHandle(m_hsemReaders);
CloseHandle(m_hsemWriters);
}

//等待 读
void SingleWriteMultipleReadGuard::WaitToRead(){
//进入临界区。防止其他线程访问成员变量
EnterCriticalSection(&m_cs);

//判断当前是否有线程在等待写资源,或正在写资源 (保证了写操作优先)
BOOL fResourceWritePending = (m_nWaitingWriters || (m_nActive < 0)); //m_nActive=-1表示有线程正在写

if(fResourceWritePending){  //有等待写资源的线程或正在写资源的线程
m_nWaitingReaders ++; //等待读资源的线程数量 ++
}else{                      //可以读
m_nActive++; //当前读资源的额线程数量++
}

//离开临界区。允许其他线程发出读写资源请求
LeaveCriticalSection(&m_cs);

//等待读信号量 有信号
WaitForSingleObject(m_hsemReaders,INFINITE);
}

//等待 写
void SingleWriteMultipleReadGuard::WaitToWrite(){
//进入临界区。防止其他线程访问成员变量
EnterCriticalSection(&m_cs);

//判断是否有其他线程在读\写资源
BOOL fResourceOwned = (m_nActive != 0); //资源是否被其他线程占用

if(fResourceOwned){         //资源被其他线程占用
m_nWaitingWriters ++;
}else{ //资源空闲
m_nActive = -1;
}

//离开临界区。允许其他线程发出读写资源请求
LeaveCriticalSection(&m_cs);

//资源被占用,等待
if(fResourceOwned){
WaitForSingleObject(m_hsemWriters,INFINITE);
}
}

//读写完成,释放资源
void SingleWriteMultipleReadGuard::Done(){
//进入临界区。防止其他线程访问成员变量
EnterCriticalSection(&m_cs);

if(m_nActive > 0){  //都是读线程
m_nActive –;   //读线程数量 —
}
else if(m_nActive == -1){ //当前要释放的是写线程
m_nActive = 0;
}

HANDLE hsem = NULL; //保存要释放读信号量还是写信号量
LONG lCount = 1;    //保存要释放的资源数量。写信号量为1

//释放之后,没有线程读写资源,则唤醒等待的读或写线程
if(m_nActive == 0){
if(m_nWaitingWriters > 0){  //有线程等待写,则唤醒一个写进程
m_nActive = -1;
m_nWaitingWriters –;
hsem = m_hsemWriters;
}else if(m_nWaitingReaders > 0){  //有线程等待读,则唤醒所有的读线程
m_nActive = m_nWaitingReaders; //
m_nWaitingReaders = 0;
hsem = m_hsemReaders;
lCount = m_nActive;            //释放m_nActive个,唤醒所有等待读的线程
}
}

//离开临界区
LeaveCriticalSection(&m_cs);

//释放资源
if(hsem != NULL){
ReleaseSemaphore(hsem,lCount,NULL);
}
}

SingleWriteMultipleReadGuard guard;

//读线程 回调函数
DWORD WINAPI ReadThreadProc(LPVOID lpParam){
printf("wait to read \n");
guard.WaitToRead();
printf("read\n");
guard.Done();
printf("read done\n\n");
return 0;
}

//写线程回调函数
DWORD WINAPI WriteThreadProc(LPVOID lpParam){
printf("wait to write\n");
guard.WaitToWrite();
printf("write\n");
guard.Done();
printf("write done\n\n");
return 0;
}

int main()
{
int i=0;
HANDLE h[THREAD_COUNT];

//随机产生读写线程
srand((unsigned int)time(0));
for (i=0;i<THREAD_COUNT;i++){
if(rand()%2){
h[i] = (HANDLE)_beginthreadex(NULL,0,(PTHREAD_START) ReadThreadProc,NULL,0,NULL);
}else{
h[i] = (HANDLE)_beginthreadex(NULL,0,(PTHREAD_START) WriteThreadProc,NULL,0,NULL);
}
}

WaitForMultipleObjects(THREAD_COUNT,h,TRUE,INFINITE);
for (i=0;i<THREAD_COUNT;i++){
CloseHandle(h[i]);
}

printf("End\n");
return 0;
}[/cpp]

作者:JarvisChu
原文链接:Windows 核心编程 学习笔记 (第四部分)
版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0

发表评论