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

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

May 9, 2014
Coding
Windows, C++

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 临界区 (关键代码段) #

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

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

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

[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;
}