Windows 核心编程 学习笔记(第四部分)
May 9, 2014
19. 用户态的线程同步方式 #
(1) 互锁函数 – InterLocked 函数族 #
常用的互锁函数有InterLockedAdd, InterLockedExchange,InterLockedExchangeAdd,InterLockedCompareExchange等
//原子操作:加法 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
);
互锁函数运行速度很快,通常只需几个CPU周期(通常小于50)的执行时间,并且不会从用户方式转化为内核方式。
(2) 循环锁 #
BOOL g_fResourceInUse = FALSE; //全局标识,用于互斥访问
void Fun(){
//循环等待,直到标识的前一状态是FALSE
while (InterlockedExchange(&g_fResourceInUse,TRUE) == TRUE){
Sleep(0); //
}
//…访问互斥资源
//标识置为 FALSE
InterlockedExchange(&g_fResourceInUse,FALSE);
}
缺点:浪费CPU时间。CPU会不断的执行循环(即便使用Sleep,也会存在浪费,因为Sleep(0) 实际是只是放弃当前的CPU时间片,线程依然是可调度的)。可以通过延长Sleep的时间来减少CPU时间的浪费。
应该避免在单个CPU的计算机上使用循环锁。在多处理器的计算机上可以使用,因为此时一个线程循环执行时,另一个线程可以在另一个CPU上运行。
(3) CRITICAL_SECTION 临界区 (关键代码段) #
优点:内部使用了互锁函数,所以运行效率高。
缺点:无法对多个进程中的线程进程同步
[1] 定义临界区
CRITICAL_SECTION 结构体的定义在WinNT.h中
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;
我们不需要关心其内部的成员变量。只要会用CRITICAL_SECTION 即可。
CRITICAL_SECTION cs; //[1]定义临界区结构体
[2] 初始化临界区 – InitializeCritialSection
void WINAPI InitializeCriticalSection( //初始化临界区
__out LPCRITICAL_SECTION lpCriticalSection //指向临界区变量的指针
);
如果线程进入未初始化的临界区,其结果是未定义的。
InitializeCriticalSection(&cs); //[2]初始化临界区
还可以使用InitializeCriticalSectionAndSpinCount 函数初始化一个自旋的临界区。所谓自旋,指当一个线程试图进入一个已锁的临界区,线程会进入一个循环,检测锁是否被释放,如果锁没有被释放,则线程进入sleep.(自旋就是循环检测,如果已锁,则sleep一下,醒来继续检测)
BOOL WINAPI InitializeCriticalSectionAndSpinCount(
__out LPCRITICAL_SECTION lpCriticalSection, //临界区
__in DWORD dwSpinCount //自旋次数,即循环的次数
);
在单处理器系统上,自旋次数参数会被忽略且设置为0。在多处理器系统上,如果临界区已锁,线程会自旋dwSpinCount次,如果自旋dwSpinCount次后锁仍为释放,则进入等待(wait)。[sleep会自动唤醒,wait需等待其它线程唤醒]
[3] 进入临界区 – EnterCriticalSection
void WINAPI EnterCriticalSection( //进入临界区:等待(堵塞),直到获取锁
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
进入等待状态的线程不消耗CPU。系统会记住该线程想要访问该资源且自动更新CRITICAL_SECTION的成员变量,一旦目前访问该资源的线程调用了LeaveCriticalSection,等待的线程就切换到可调度状态。
还可以使用TryEnterCriticalSection 函数,无论临界区是否已锁,都会立即返回。
BOOL WINAPI TryEnterCriticalSection( //如果当前线程成功进入临界区或已经进入临界区,返回非0值。
_Inout_ LPCRITICAL_SECTION lpCriticalSection //如果临界区被其他线程占用,则返回0
);
[4] 退出临界区 – LeaveCriticalSection
void WINAPI LeaveCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
一个线程可以多次EnterCriticalSection 而不会死锁,但一个Enter必须对应一个Leave。
[5] 删除临界区 – DeleteCriticalSection
void WINAPI DeleteCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
临界区使用的技巧
- 每个共享资源使用一个CRITICAL_SECTION
- 同时Enter多个临界区时,注意Enter和Leave的顺序,避免形成死锁。如果两个线程同时执行下面的代码,会造成死锁。
EnterCriticalSection(&cs1);
EnterCriticalSection(&cs2);
//…
LeaveCriticalSection(&cs1); //会产生死锁。应该先Leave cs2,再Leave cs1.
LeaveCriticalSection(&cs2);
- 不要长时间运行临界区。临界区访问越短越好。
20. 内核态的线程同步方式 #
(1) 内核对象用于线程同步 #
Windows中的一些内核对象拥有 **已通知(TRUE)和未通知(FALSE)**两种状态(即有信号和无信号两种状态),可以用于线程同步。每个对象的状态切换都有自己的一套规则。
这些内核对象包括:进程、线程、作业、文件、控制台输入、文件修改通知、可等待定时器、事件、Semaphore、Mutex。
以进程为例:进程总是在未通知状态中创建,正在运行的进程对象处于未通知状态,进程终止时就变味已通知状态。
所以可以使用WaitForSingleObject 等待进程的句柄来判断进程是否终止。
其中:可等待定时器、Event、Semaphore、Mutex 是Windows提供的专门用于线程同步的内核对象。
当线程等待的对象处于未通知状态,则线程不可调度的,当等待的对象变为已通知状态,则线程切换到可调度状态。
(2) 等待函数 #
等待函数使线程自愿进入等待状态,直到一个特定的内核对象变为已通知状态。
[1] WaitForSingleObject – 等待一个内核对象
DWORD WINAPI WaitForSingleObject( //等待一个内核对象变为已通知状态
_In_ HANDLE hHandle, //等待的内核对象句柄
_In_ DWORD dwMilliseconds //等待时间(单位ms),可以传INFINITE表示一直等待,直到等待对象变为已通知
);
DWORD dwRet = WaitForSingleObject(hThread,5000);
switch(dwRet){
case WAIT_OBJECT_0:
//等待内核对象变为已通知。如线程已结束
break;
case WAIT_TIMEOUT:
//等待超时
break;
case WAIT_FAILED:
//函数调用失败(如INVALID_HANDLE)。使用GetLastError获取具体信息.
break;
}
[2] WaitForMultipleObjects – 等待多个内核对象
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)
);
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;
}
[3] 成功等待的副作用
对于某些内核对象,成功的调用了WaitForSingleObject和WaitForMultipleObject时会改变对象的状态,这就是成功等待的副作用。
不同的内核对象的成功等待的副作用不一样。如自动重置事件(AutoResetEvent)对象的成功等待的副作用是 即WaitFor函数返回前,该对象会被置为未通知状态。有些内核对象如线程、进程成功等待也不会有副作用
等待不成功绝不会有副作用。
WaitForMultipleObject 是以原子方式运行的,可以避免死锁的产生。如两个线程同时运行下面的代码:
HANDLE h[2];
h[0] = hAutoResetEvent1; //初始为未通知状态
h[1] = hAutoResetEvent2; //初始为未通知状态
WaitForMultipleObjects(2,h,TRUE,INFINITE);
初始时两个对象都为未通知状态,两个线程都进入等待。
然后,当hAutoResetEvent1变为已通知时,两个线程都能发现该状态改变,但是都无法唤醒,因为hAutoResetEvent2仍处于未通知,所有两个线程都没有成功等待,不会对hAutoResetEvent1对象产生副作用(即没有将它置为未通知状态)
然后,当红AutoResetEvent2变为已通知时,两个线程中的一个线程发现该变化,两个对象都变为已通知状态,成功等待,由于副作用的存在,会把两个对象会置为未通知状态,该线程变为可调度线程。此时,另一个线程将继续等待,直到前一个线程的WaitForMultpleObject函数返回,两个对象重新变会已通知状态。
这样就避免了两个线程的死锁。
如果WaitForMultipleObject不是原子操作的,那么可能发生一个线程将hAutoResetEvent1置为未通知状态,另一个线程将hAutoResetEvent2置为未通知状态,两个线程就会互相等待,形成死锁。
【至于两个线程同时发现内核对象已通知状态后,系统究竟会执行哪个,这个是不确定的。Microsoft只说使用的算法是公平的,但具体的算法不知道。实际操作是FIFO算法,等待时间最长的线程获得该对象,但也不是绝对】
(3) Event – 事件内核对象 #
Event是最基本的内核对象。包括一个使用计数(所有内核对象都有)、一个指明该Event是自动重置事件还是手动重置事件的BOOL值、一个指明该Event处在已通知状态还是未通知状态的BOOL值。
- 自动重置事件:事件被设置为已通知(有信号)状态后,会唤醒一个等待线程,然后自动恢复为未通知(无信号)状态。
- 手动重置事件:事件被设置为已通知状态后,事件会保持已通知状态,直到使用ResetEvent重置为未通知状态。当事件处于已通知状态时,所有等待事件的线程都会等到事件,并被唤醒为可调度状态。
可见两者的区别在于:自动重置事件一次只能唤醒一个等待线程,手动重置事件会唤醒所有的等待线程。
自动重置事件会有成功等待副作用(如上),手动重置事件没有成功等待副作用
[1] 创建事件对象 – CreateEvent
HANDLE WINAPI CreateEvent( //创建事件对象,返回其句柄
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes, //安全属性,可以为NULL
_In_ BOOL bManualReset, //TRUE表示手动重置事件,FALSE表示自动重置事件
_In_ BOOL bInitialState,//初始状态,TRUE表示已通知,FALSE表示未通知
_In_opt_ LPCTSTR lpName //内核对象名称,可以为NULL
);
[2] 设置事件对象状态 – SetEvetn,ResetEvent
BOOL SetEvent( __in HANDLE hEvent ); //事件hEvent置为已通知状态。成功返回非0,否则返回0
BOOL ResetEvent( __in HANDLE hEvent );//事件hEvent置为未通知状态。成功返回非0,否则返回0
(4) WaitableTimer – 等待定时器内核对象 #
等待定时器对象是一个在某个时间或每隔规定时间间隔变为有信号(已通知)状态的内核对象。
- 自动重置等待定时器(又称同步定时器):一个等待线程等到之后,会置为未通知状态。只能唤醒一个等待线程。
- 手动重置等待定时器:会保持已通知状态,直到SetWaitableTimer。可以唤醒所有的等待线程。
[1] CreateWaitableTimer – 创建等待定时器内核对象
HANDLE WINAPI CreateWaitableTimer( //创建等待定时器内核对象,初始为未通知状态
_In_opt_ LPSECURITY_ATTRIBUTES lpTimerAttributes,
_In_ BOOL bManualReset, //TRUE:手动重置等待定时器,FALSE:自动重置等待定时器
_In_opt_ LPCTSTR lpTimerName
);
[2] SetWaitableTimer – 设置等待定时器内核对象
刚创建的定时器对象是未通知状态的,必须使用SetWaitableTimer设置其合适变为已通知状态
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。
);
- pDueTime。正值表示一个绝对时间,负值表示相对时间,即SetWaitableTimer函数调用后多久报时。
- fResume。TRUE表示当定时器报时时,它是计算机摆脱暂停状态(如果它处在暂停状态的话),并唤醒等待定时器的线程。FALSE表示当定时器报时时,如果计算机处于暂停状态,它唤醒的线程必须等到计算机恢复运行后才能被调度。
绝对时间报时的例子
#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;
}
相对时间报时的例子:与绝对时间类似,只是时间设置不同
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报时一次
[3] CancelWaitableTimer – 取消定时器
不再报时
[4] WaitableTimer 和 SetTimer比较
SetTimer 创建的定时器称为用户定时器。
需要使用窗口UI结构,产生WM_TIMER消息,消息只能被调用线程获取,即只有一个线程能得到通知。WM_TIMER是优先级最低的消息,只有消息队列中没有其他消息时,才会获取该消息。
WaitableTimer
内核对象,可供多个线程共享,且是安全的。可以唤醒多个等待线程。
(5) Semaphore – 信号量内核对象 #
信号量用于对资源进行计数,可以实现资源的互斥和共享,能够控制同时访问资源的线程数量。
信号量有两个计数:一个是最大的资源数量,一个是当前资源数量。
- 当前资源数量 >0,则发出信号
- 当前资源数量 =0,则不发出信号
- 当前资源数量不会 < 0
- 当前资源数量不能超过最大资源数量
- Wait函数(WaitForSingleObject,WaitForMultipleObject)成功等待,会自动将信号量的当前资源数量 -1。
[1] CreateSemaphore – 创建信号量
HANDLE WINAPI CreateSemaphore( //创建信号量
_In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, //
_In_ LONG lInitialCount, //初始资源数量
_In_ LONG lMaximumCount, //最大资源数量
_In_opt_ LPCTSTR lpName //
);
[2] ReleaseSemaphore – 释放信号量
BOOL WINAPI ReleaseSemaphore( //释放信号量
_In_ HANDLE hSemaphore,
_In_ LONG lReleaseCount, //释放资源的数量
_Out_opt_ LPLONG lpPreviousCount //释放前的资源数量
);
使用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 – 创建互斥对象
HANDLE WINAPI CreateMutex( //创建互斥对象
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_ BOOL bInitialOwner, //当前线程是否是互斥对象的初始拥有者,通常设为FALSE:不拥有
_In_opt_ LPCTSTR lpName
);
[2] ReleaseMutex – 释放互斥对象
BOOL WINAPI ReleaseMutex(
_In_ HANDLE hMutex
);
互斥对象有个“线程所有权”的概念,如果不是拥有Mutex的线程调用ReleaseMutex释放,操作会失败。
如果拥有Mutex的线程直到终止都没有释放Mutex,那么系统会将Mutex视为已经被放弃的,等待的线程会得到一个WAIT_ABANDONED的返回值,表明拥有Mutex的线程直到终止都未释放Mutex,此时共享资源的额情况是未知的,但Mutex是可用的,获得通知的线程决定自身后续怎么做。
case WAIT_ABANDONED:
//拥有等待的内核对象的线程直到终止都没有释放内核对象.内核对象会自动被调用线程拥有,初始状态为未通知。
(7) 其他的线程同步函数 #
[1] WaitForInputIdle – 等待进程,直到其处于空闲状态
DWORD WINAPI WaitForInputIdle( //等待进程hProcess,直到该进程在<b>创建应用程序的第一个窗口的线程</b>中已经没有尚未处理的输入为止。
_In_ HANDLE hProcess, //即该进程第一个创建窗口的线程已初始化完成,并处于等待用户输入状态。
_In_ DWORD dwMilliseconds
);
如父进程创建了子进程后,想要获取子进程的创建的窗口的句柄,就可以通过调用WaitForInputIdle等待子进程窗口的初始化完成。
#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;
}
[2] MsgWaitForMultipleObject – 等待内核对象 和 消息
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
);
21. 一个互斥写和共享读锁的实现例子 #
#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;
}