CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 第1章高频C++11重难点知识解析

  • 第2章Linux GDB高级调试指南

  • 第3章C++多线程编程从入门到进阶

    • 3.1 线程的基本概念
    • 3.2 线程基本操作
    • 3.3 线程函数传C++类实例指针惯用法
    • 3.4 整型变量的原子操作
    • 3.5 Linux线程同步对象
    • 3.6 Windows 线程资源同步对象
      • 3.7 C++ 11/14/17 线程同步对象
      • 3.8 如何确保创建的线程一定运行起来?
      • 3.9 多线程使用锁实践经验总结
      • 3.10 线程局部存储
      • 3.11 C 库的非线程安全函数
      • 3.12 线程池与队列系统的设计
      • 3.13 纤程(Fiber)与协程(Coroutine)
      • 3.14 本章总结
    • 第4章C++网络编程重难点解析

    • 第5章网络通信故障排查常用命令

    • 第6章高性能网络通信协议设计精要

    • 第7章高性能服务结构设计

    • 第8章Redis 网络通信模块源码分析

    • 第9章后端服务重要模块设计探索

    • C++后端开发进阶
    • 第3章C++多线程编程从入门到进阶
    zhangxf
    2023-04-05
    目录

    3.6 Windows 线程资源同步对象

    # 3.6.1 WaitForSingleObject 与 WaitForMultipleObjects 函数

    在介绍 Windows 线程资源同步对象之前,我们先来介绍一下两个与之相关的、非常重要的函数,即WaitForSingleObject和WaitForMultipleObjects 。先来说WaitForSingleObject,这个函数的签名是:

    DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
    
    1

    这个函数的作用是等待一个内核对象,在Windows系统上一个内核对象通常使用其句柄来操作,参数hHandle即需要等待的内核对象,参数dwMilliseconds是等待这个内核对象的最大时间,时间单位是毫秒,其类型是DWORD,这是一个unsigned long类型。如果我们需要无限等待下去,可以将这个参数值设置为INFINITE宏。

    在Windows上可以调用WaitForSingleObject等待的常见对象如下表所示:

    可以被等待的对象 等待对象成功的含义 对象类型
    线程 等待线程结束 HANDLE
    Process 等待进程结束 HANDLE
    Event (事件) 等待 Event 有信号 HANDLE
    Mutex (互斥体) 等待持有 Mutex 的线程释放该 Mutex,等待成功,拥有该Mutex HANDLE
    Semaphore(信号量) 等待该 Semaphore 对象有信号 HANDLE

    上面介绍的等待线程对象上文中已经详细介绍过了,这里不再重复了,等待进程退出与等待线程退出类似,也不再赘述。下文中我们将详细介绍 Event、Mutex、Semaphore 这三种类型的资源同步对象,这里我们先接着介绍WaitForSingleObject函数的用法,该函数的返回值一般有以下类型:

    • WAIT_FAILED,表示WaitForSingleObject函数调用失败了,调用失败时,可以通过GetLastError 函数得到具体的错误码;
    • WAIT_OBJECT_0,表示WaitForSingleObject成功“等待”到设置的对象;
    • WAIT_TIMEOUT,等待超时;
    • WAIT_ABANDONED,当等待的对象是Mutex类型时,如果持有该Mutex对象的线程已经结束,但是没有在结束前释放该Mutex,此时该Mutex已经处于废弃状态,其行为是未知的,不建议再使用该Mutex。

    WaitForSingleObject如其名字一样,只能“等待”单个对象,如果需要同时等待多个对象可以使用WaitForMultipleObjects,除了对象的数量变多了,其用法基本上和WaitForSingleObject一样。 WaitForMultipleObjects函数签名如下:

    DWORD WaitForMultipleObjects(
        DWORD        nCount,
        const HANDLE *lpHandles,
        BOOL         bWaitAll,
        DWORD        dwMilliseconds
    );
    
    1
    2
    3
    4
    5
    6

    参数lpHandles是需要等待的对象数组指针,参数nCount指定了该数组的长度,参数bWaitAll表示是否等待数组lpHandles所有对象有“信号”,取值为TRUE 时,WaitForMultipleObjects会等待所有对象有信号才会返回,取值为FALSE时,当其中有一个对象有信号时,立即返回,此时其返回值表示哪个对象有信号。

    在参数bWaitAll设置为 FALSE 的情况下, 除了上面介绍的返回值是WAIT_FAILED和WAIT_TIMEOUT以外,返回值还有另外两种情形(分别对应WaitForSingleObject返回值是WAIT_OBJECT_0 和WAIT_ABANDONED两种情形):

    • WAIT_OBJECT_0 ~ (WAIT_OBJECT_0 + nCount– 1),举个例子,假设现在等待三个对象A1、A2、A3,它们在数组lpHandles中的下标依次是0、1、2,某次WaitForMultipleObjects返回值是 Wait_OBJECT_0 + 1,则表示对象A2有信号,导致WaitForMultipleObjects调用成功返回。

      伪码如下:

      HANDLE waitHandles[3];
      waitHandles[0] = hA1Handle;
      waitHandles[1] = hA2Handle;
      waitHandles[2] = hA3Handle;
      
      DWORD dwResult = WaitForMultipleObjects(3, waitHandles, FALSE, 3000);
      switch(dwResult)
      {
          case WAIT_OBJECT_0 + 0:
              //A1 有信号
              break;
      
          case WAIT_OBJECT_0 + 1:
              //A2 有信号
              break;
      
          case WAIT_OBJECT_0 + 2:
              //A3 有信号
              break;
      
          default:
              //出错或超时
              break;
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
    • WAIT_ABANDONED_0 ~ (WAIT_ABANDONED_0 + nCount– 1),这种情形与上面的使用方法相同,通过nCount - 1可以知道是等待对象数组中哪个对象始终没有被其他线程释放使用权。

    这里说了这么多理论知识,读者将在下文介绍的Windows常用的资源同步对象章节中看到具体的示例代码。

    # 3.6.2 Windows的临界区对象

    在所有的Windows资源同步对象中,CriticalSection(临界区对象,有些书上翻译成“关键段”)是最简单易用的,从程序的术语来说,它防止多线程同时执行其保护的那段代码(临界区代码),即临界区代码某一时刻只允许一个线程去执行,示意图如下:

    Windows没有公开CriticalSection数据结构的定义,我们一般使用如下五个API函数操作临界区对象:

    void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
    void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
    
    BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
    void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
    void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
    
    1
    2
    3
    4
    5
    6

    InitializeCriticalSection和DeleteCriticalSection用于初始化和销毁一个CRITICAL_SECTION对象;位于EnterCriticalSection和LeaveCriticalSection之间的代码即临界区代码;调用EnterCriticalSection的线程会尝试“进入“临界区,如果进入不了,则会阻塞调用线程,直到成功进入或者超时;TryEnterCriticalSection会尝试进入临界区,如果可以进入,则函数返回TRUE ,如果无法进入则立即返回不会阻塞调用线程,函数返回FALSE。LeaveCriticalSection函数让调用线程离开临界区,离开临界区以后,临界区的代码允许其他线程调用EnterCriticalSection进入。

    EnterCriticalSection 超时时间很长,可以在注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager这个位置修改参数CriticalSectionTimeout的值调整,当然实际开发中我们从来不会修改这个值,如果你的代码等待时间较长最终超时,请检查你的逻辑设计是否合理。

    我们来看一段实例代码:

    1  #include <Windows.h>
    2  #include <list>
    3  #include <iostream>
    4  #include <string>
    5 
    6  CRITICAL_SECTION       g_cs;
    7  int                    g_number = 0;
    8
    9  DWORD __stdcall WorkerThreadProc(LPVOID lpThreadParameter)
    10 {
    11    DWORD dwThreadID = GetCurrentThreadId();
    12    
    13    while (true)
    14    {
    15        EnterCriticalSection(&g_cs);
    16		  std::cout << "EnterCriticalSection, ThreadID: " << dwThreadID << std::endl;
    17        g_number++;
    18        SYSTEMTIME st;
    19        //获取当前系统时间
    20        GetLocalTime(&st);
    21        char szMsg[64] = { 0 };
    22        sprintf(szMsg, 
    23                "[%04d-%02d-%02d %02d:%02d:%02d:%03d]NO.%d, ThreadID: %d.", 
    24                st.wYear, st.wMonth, st.wDay, 
    25				  st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, 
    26                g_number, dwThreadID);
    27
    28        std::cout << szMsg << std::endl;
    29        std::cout << "LeaveCriticalSection, ThreadID: " << dwThreadID << std::endl;
    30        LeaveCriticalSection(&g_cs);
    31
    32        //睡眠1秒
    33        Sleep(1000);
    34    }
    35
    36    return 0;
    37 }
    38
    39 int main()
    40 {
    41    InitializeCriticalSection(&g_cs);
    42
    43    HANDLE hWorkerThread1 = CreateThread(NULL, 0, WorkerThreadProc, NULL, 0, NULL);
    44    HANDLE hWorkerThread2 = CreateThread(NULL, 0, WorkerThreadProc, NULL, 0, NULL);
    45
    46    WaitForSingleObject(hWorkerThread1, INFINITE);
    47    WaitForSingleObject(hWorkerThread2, INFINITE);
    48
    49    //关闭线程句柄
    50    CloseHandle(hWorkerThread1);
    51    CloseHandle(hWorkerThread2);
    52
    53    DeleteCriticalSection(&g_cs);
    54
    55    return 0;
    56 }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56

    上述程序执行输出结果如下:

    EnterCriticalSection, ThreadID: 1224
    [2019-01-19 22:25:41:031]NO.1, ThreadID: 1224.
    LeaveCriticalSection, ThreadID: 1224
    EnterCriticalSection, ThreadID: 6588
    [2019-01-19 22:25:41:031]NO.2, ThreadID: 6588.
    LeaveCriticalSection, ThreadID: 6588
    EnterCriticalSection, ThreadID: 6588
    [2019-01-19 22:25:42:031]NO.3, ThreadID: 6588.
    LeaveCriticalSection, ThreadID: 6588
    EnterCriticalSection, ThreadID: 1224
    [2019-01-19 22:25:42:031]NO.4, ThreadID: 1224.
    LeaveCriticalSection, ThreadID: 1224
    EnterCriticalSection, ThreadID: 1224
    [2019-01-19 22:25:43:031]NO.5, ThreadID: 1224.
    LeaveCriticalSection, ThreadID: 1224
    EnterCriticalSection, ThreadID: 6588
    [2019-01-19 22:25:43:031]NO.6, ThreadID: 6588.
    LeaveCriticalSection, ThreadID: 6588
    EnterCriticalSection, ThreadID: 1224
    [2019-01-19 22:25:44:031]NO.7, ThreadID: 1224.
    LeaveCriticalSection, ThreadID: 1224
    EnterCriticalSection, ThreadID: 6588
    [2019-01-19 22:25:44:031]NO.8, ThreadID: 6588.
    LeaveCriticalSection, ThreadID: 6588
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    上述代码中我们新建两个工作线程,线程函数都是WorkerThreadProc。线程函数在15行调用EnterCriticalSection进入临界区,在30行调用LeaveCriticalSection离开临界区,16~29行之间的代码即临界区的代码,这段代码由于受到临界区对象g_cs的保护,因为每次只允许一个工作线程执行这段代码。虽然临界区代码中有多个输出,但是这些输出一定都是连续的,不会出现交叉输出的结果。

    细心的读者会发现上述输出中存在同一个的线程连续两次进入临界区,这是有可能的。也就是说,当其中一个线程离开临界区,即使此时有其他线程在这个临界区外面等待,由于线程调度的不确定性,此时正在等待的线程也不会有先进入临界区的优势,它和刚离开这个临界区的线程再次竞争进入临界区是机会均等的。我们来看一张图:

    上图中我们将线程函数的执行流程绘制成一个流程图,两个线程竞争进入临界区可能存在如下情形,为了表述方便,我们将线程称A、B。

    • 情形一:线程A被唤醒获得CPU时间片进入临界区,执行流程①,然后执行临界区代码输出 -> 线程B获得CPU时间片,执行流程②,然后失去CPU时间片进入休眠 -> 线程A执行完临界区代码离开临界区后执行流程⑤,然后失去CPU时间片进入休眠 -> 线程B被唤醒获得CPU时间片执行流程③、①,然后执行临界区代码输出。

      这种情形下,线程A和线程B会轮流进入临界区执行代码。

    • 情形二:线程A被唤醒获得CPU时间片进入临界区,执行流程①,然后执行临界区代码输出 -> 线程B获得CPU时间片,执行流程③,然后执行流程②在临界区外面失去CPU时间片进入休眠 -> 线程A执行完临界区代码离开临界区后执行流程④、① 。

      这种情形下,会出现某个线程连续两次甚至更多次的进入临界区执行代码。

    如果某个线程在尝试进入临界区时因阻塞而进入睡眠状态,当其他线程离开这个临界区后,之前因为这个临界区而阻塞的线程可能会被唤醒进行再次竞争,也可能不被唤醒。但是存在这样一种特例,假设现在存在两个线程A和B,线程A为离开临界区的线程且不需要再次进入临界区,那么线程B在被唤醒时一定可以进入临界区。线程B从睡眠状态被唤醒,这涉及到一次线程的切换,有时候这种开销是不必要的,我们可以让B执行一个简单的循环等待一段时间后再进入临界区,而不是先睡眠再唤醒,前者与后者相比,执行这个循环的消耗更小。这就是所谓的“自旋”,在这种情形下,Windows 提供了另外一个初始化临界区的函数InitializeCriticalSectionAndSpinCount,这个函数比InitializeCriticalSection多一个自旋的次数:

    BOOL InitializeCriticalSectionAndSpinCount(
          LPCRITICAL_SECTION lpCriticalSection,
          DWORD              dwSpinCount
    );
    
    1
    2
    3
    4

    参数dwSpinCount是自旋的次数,利用自旋来避免线程因为等待而进入睡眠、然后再次被唤醒,消除线程上下文切换带来的消耗,提高效率。当然,在实际开发中这种方式是靠不住的,线程调度是操作系统内核的策略,应用层上的应用不应该假设线程的调度策略是按预想的来执行,但是理解线程与临界区之间的原理有助于你编写出更高效的代码来。

    需要说明的是,临界区对象通过保护一段代码不被多个线程同时执行,进而来保证多个线程之间读写一个对象是安全的。由于同一时刻只有一个线程可以进入临界区,因此这种对资源的操作是排他的,即对于同一个临界区对象,不会出现多个线程同时操作该资源,哪怕是资源本身可以在同一时刻被多个线程进行操作,如多个线程对资源进行读操作,这就带来了效率问题。

    我们一般将进入临界区的线程称为该临界区的拥有者(owner)——临界区持有者。

    最后,为了避免死锁,EnterCriticalSection和LeaveCriticalSection需要成对使用,尤其是在具有多个出口的函数中,记得在每个分支处加上LeaveCriticalSection。伪码如下:

    void someFunction()
    {
        EnterCriticalSection(&someCriticalSection);
        if (条件A)
        {
            if (条件B)
            {
                LeaveCriticalSection(&someCriticalSection);
                //出口1
                return;
            }
    
            LeaveCriticalSection(&someCriticalSection);
            //出口2
            return;
        }
    
        if (条件C)
        {
            LeaveCriticalSection(&someCriticalSection);
            // 出口3
            return;
        }
    
        if (条件C)
        {
            LeaveCriticalSection(&someCriticalSection);
            // 出口4
            return;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31

    上述代码中,为了能让临界区对象被正常的释放,在函数的每个出口都加上了 LeaveCriticalSection 调用,如果函数的出口非常多,这样的代码太难维护了。所以一般建议使用 RAII 技术将临界区API封装成对象,该对象在其作用域内调用构造函数进入临界区,在出了其作用域后调用析构函数离开临界区,示例代码如下:

    class CCriticalSection
    {
    public:
        CCriticalSection(CRITICAL_SECTION& cs) : mCS(cs)
        {
            EnterCriticalSection(&mCS);
        }
    
        ~CCriticalSection()
        {
            LeaveCriticalSection(&mCS);
        }
    
    private:
        CRITICAL_SECTION& mCS;
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    利用 CCriticalSection 类,我们可以对上述伪码进行优化:

    void someFunction()
    {
        CCriticalSection autoCS(someCriticalSection);
        if (条件A)
        {
            if (条件B)
            { 
                //出口1
                return;
            }
       
            //出口2
            return;
        }
    
        if (条件C)
        {      
            // 出口3
            return;
        }
    
        if (条件C)
        {        
            // 出口4
            return;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    上述代码中,变量autoCS会在出了函数作用域后调用其析构函数,在析构函数中调用LeaveCriticalSection自动离开临界区。

    # 3.6.3 Windows Event对象

    本节讨论的Event对象不是Windows UI事件驱动机制中的事件,而是多线程资源同步中的Event对象,它也是Windows内核对象之一。在Windows多线程程序设计中,使用频率较高,我们先来学习一下如何创建Event对象,然后逐步展开。创建Event的Windows API函数签名是:

    HANDLE CreateEvent(
      LPSECURITY_ATTRIBUTES lpEventAttributes,
      BOOL                  bManualReset,
      BOOL                  bInitialState,
      LPCTSTR               lpName
    );
    
    1
    2
    3
    4
    5
    6

    参数和返回值说明:

    • 参数lpEventAttributes,这个参数设置了Event对象的安全属性,Windows中所有的内核对象都可以设置这个属性,我们一般设置为NULL,即使用默认安全属性。
    • 参数bManualReset,这个参数设置Event对象受信(变成有信号状态)时的行为,当设置为TRUE时,表示需要手动调用ResetEvent函数去将Event重置成无信号状态;当设置为FALSE,Event事件对象受信后会自动重置为无信号状态。
    • 参数bInitialState设置Event事件对象初始状态是否是受信的,TRUE表示有信号,FALSE表示无信号。
    • 参数lpName可以设置Event对象的名称,如果不需要设置名称,可以将该参数设置为NULL。一个Event对象根据是否设置了名称分为具名对象(具有名称的对象)和匿名对象。Event对象是可以通过名称在不同进程之间共享的,通过这种方式共享很有用,我们在后面章节中会详细介绍如何共享。
    • 返回值,如果成功创建Event对象返回对象的句柄,如果创建失败返回NULL。

    一个无信号的Event对象,我们可以通过SetEvent将其变成受信状态,SetEvent的函数签名如下:

    BOOL SetEvent(HANDLE hEvent);
    
    1

    我们将参数hEvent设置为我们需要设置信号的Event句柄即可。

    同理,一个已经受信的Event对象,我们可以使用ResetEvent对象将其变成无信号状态,ResetEvent的函数签名如下:

    BOOL ResetEvent(HANDLE hEvent);
    
    1

    参数hEvent 即我们需要重置的Event对象句柄。

    说了这么多,我们来看一个具体的例子。假设我们现在有两个线程,其中一个是主线程,主线程等待工作线程执行某一项耗时的任务完成后,将任务结果显示出来。代码如下:

    #include <Windows.h>
    #include <string>
    #include <iostream>
    
    bool        g_bTaskCompleted = false;
    std::string g_TaskResult;
    
    DWORD __stdcall WorkerThreadProc(LPVOID lpThreadParameter)
    {
        //使用Sleep函数模拟一个很耗时的操作
        //睡眠3秒
        Sleep(3000);
        g_TaskResult = "task completed";
        g_bTaskCompleted = true;
    
        return 0;
    }
    
    int main()
    {
        HANDLE hWorkerThread = CreateThread(NULL, 0, WorkerThreadProc, NULL, 0, NULL); 
        while (true)
        {
            if (g_bTaskCompleted)
            {
                std::cout << g_TaskResult << std::endl;
                break;
            }        
            else
                std::cout << "Task is in progress..." << std::endl;
        }
        
        CloseHandle(hWorkerThread);
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

    程序执行结果如下图所示:

    上述代码中,主线程为了等待工作线程完成任务后获取结果,使用了一个循环去不断查询任务完成标识,这是很低效的一种做法,等待的线程(主线程)做了很多无用功,对CPU时间片也是一种浪费。我们使用Event对象来改写一下上述代码:

    1  #include <Windows.h>
    2  #include <string>
    3  #include <iostream>
    4 
    5  bool        g_bTaskCompleted = false;
    6  std::string g_TaskResult;
    7  HANDLE      g_hTaskEvent = NULL;
    8 
    9  DWORD __stdcall WorkerThreadProc(LPVOID lpThreadParameter)
    10 {
    11    //使用Sleep函数模拟一个很耗时的操作
    12    //睡眠3秒
    13    Sleep(3000);
    14    g_TaskResult = "task completed";
    15    g_bTaskCompleted = true;
    16
    17    //设置事件信号
    18    SetEvent(g_hTaskEvent);
    19
    20    return 0;
    21 }
    22
    23 int main()
    24 {
    25    //创建一个匿名的手动重置初始无信号的事件对象
    26    g_hTaskEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
    27    HANDLE hWorkerThread = CreateThread(NULL, 0, WorkerThreadProc, NULL, 0, NULL); 
    28    
    29    DWORD dwResult = WaitForSingleObject(g_hTaskEvent, INFINITE);
    30    if (dwResult == WAIT_OBJECT_0)
    31    {
    32        std::cout << g_TaskResult << std::endl;
    33    }
    34    
    35    CloseHandle(hWorkerThread);
    36    CloseHandle(g_hTaskEvent);
    37    return 0;
    38 }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

    上述代码中,主线程在工作线程完成任务之前会一直阻塞代码29行,没有任何消耗,当工作线程完成任务后调用SetEvent让事件对象受信,这样主线程会立即得到通知,从WaitForSingleObject返回,此时任务已经完成,就可以得到任务结果了。

    在实际的开发中,我们可以利用等待的时间去做一点其他的事情,在我们需要的时候去检测一下事件对象是否有信号即可。另外,Event对象有两个显著的特点:

    • 与临界区对象(以及接下来要介绍的Mutex对象)相比,Event对象没有让持有者线程变成其owner这一说法,所以Event对象可以同时唤醒多个等待的工作线程。
    • 手动重置的Event对象一旦变成受信状态,其信号不会丢失,也就是说当Event从无信号变成有信号时,即使某个线程当时没有调用WaitForSingleObject等待该Event对象受信,而是在这之后才调用WaitForSingleObject,仍然能检测到事件的受信状态,即不会丢失信号,而后面要介绍的条件变量就可能会丢失信号。

    蘑菇街开源的即时通讯Teamtalk pc版(代码下载地址参见链接12)在使用socket连接服务器时,使用Event对象设计了一个超时做法。传统的做法是将socket设置为非阻塞的,调用完connect函数之后,调用select函数检测socket是否可写,在select函数里面设置超时时间。Teamtalk的做法如下:

     //TcpClientModule_Impl.cpp 145行
     IM::Login::IMLoginRes* TcpClientModule_Impl::doLogin(CString &linkaddr, UInt16 port
    	,CString& uName,std::string& pass)
    {
    	//imcore::IMLibCoreConnect 中通过connect连接服务器
    	m_socketHandle = imcore::IMLibCoreConnect(util::cStringToString(linkaddr), port);
    	imcore::IMLibCoreRegisterCallback(m_socketHandle, this);
    	if(util::waitSingleObject(m_eventConnected, 5000))
    	{
    		IM::Login::IMLoginReq imLoginReq;
    		string& name = util::cStringToString(uName);
    		imLoginReq.set_user_name(name);
    		imLoginReq.set_password(pass);
    		imLoginReq.set_online_status(IM::BaseDefine::USER_STATUS_ONLINE);
    		imLoginReq.set_client_type(IM::BaseDefine::CLIENT_TYPE_WINDOWS);
    		imLoginReq.set_client_version("win_10086");
    
    		if (TCPCLIENT_STATE_OK != m_tcpClientState)
    			return 0;
    
    		sendPacket(IM::BaseDefine::SID_LOGIN, IM::BaseDefine::CID_LOGIN_REQ_USERLOGIN, ++g_seqNum
    			, &imLoginReq);
    		m_pImLoginResp->Clear();
    		util::waitSingleObject(m_eventReceived, 10000);
    	}
    
    	return m_pImLoginResp;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    util::waitSingleObject即封装API WaitForSingleObject函数:

    //utilCommonAPI.cpp 197行
    BOOL waitSingleObject(HANDLE handle, Int32 timeout)
    {
    	int t = 0;
    	DWORD waitResult = WAIT_FAILED;
    	do
    	{
    		int timeWaiter = 500;
    		t += timeWaiter;
    		waitResult = WaitForSingleObject(handle, timeWaiter);
    	} while ((WAIT_TIMEOUT == waitResult) && (t < timeout));
    
    	return (WAIT_OBJECT_0 == waitResult);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    等待的m_eventConnected对象即是一个Event类型:

    //定义
    HANDLE							m_eventConnected;
    //在TcpClientModule_Impl构造函数中初始化
    //m_eventConnected = CreateEvent(NULL, FALSE, FALSE, NULL);
    
    1
    2
    3
    4

    这个WaitForSingleObejct何时会返回呢?如果网络线程中connect函数可以正常连接服务器,会让m_eventConnected受信,这样WaitForSingleObejct 函数就会返回了,接着组装登录数据包接着发数据。

    void TcpClientModule_Impl::onConnectDone()
    {
    	m_tcpClientState = TCPCLIENT_STATE_OK;
    	::SetEvent(m_eventConnected);
    
    	m_bDoReloginServerNow = FALSE;
    	if (!m_pServerPingTimer)
    	{
    		_startServerPingTimer();
    	}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    归纳起来,这里利用了一个Event对象实现了一个同步登录的过程,网络连接最大超时事件设置成了5000毫秒(5秒):

    util::waitSingleObject(m_eventConnected, 5000)
    
    1

    # 3.6.4 Windows Mutex 对象

    Mutex( 互斥体)采用的是英文 mutual exclusive(互相排斥之意)的缩写。见名知义,Windows 中的 Mutex 对象在同一个时刻最多只能属于一个线程,当然也可以也不属于任何线程,获得 Mutex 对象的线程成为该 Mutex 的拥有者(owner)。我们可以在创建 Mutex 对象时设置 Mutex 是否属于创建它的线程,其他线程如果希望获得该 Mutex,则可以调用 WaitForSingleObject 进行申请。创建 Mutex 的 API 是 CreateMutex,其函数签名如下:

    HANDLE CreateMutex(
          LPSECURITY_ATTRIBUTES lpMutexAttributes,
          BOOL                  bInitialOwner,
          LPCTSTR               lpName
    );
    
    1
    2
    3
    4
    5

    参数和返回值说明:

    • 参数 lpMutexAttributes 用法同 CreateEvent,上面已经介绍过了,一般设置为 NULL;
    • 参数 bInitialOwner,设置调用 CreateMutex 的线程是否立即拥有该 Mutex 对象,TRUE 拥有,FALSE 不拥有,不拥有时,其他线程调用 WaitForSingleObject 可以获得该 Mutex 对象;
    • 参数 lpName,Mutex 对象的名称,Mutex 对象和 Event 对象一样,也可以通过名称在多个线程之间共享,如果不需要名称可以将该参数设置为 NULL,根据是否具有名称 Mutex 对象分为具名 Mutex 和匿名 Mutex;
    • 返回值,如果函数调用成功返回 Mutex 的句柄,调用失败返回 NULL。

    当一个线程不再需要该 Mutex,可以使用 ReleaseMutex 函数释放 Mutex,让其他需要等待该 Mutex 的线程有机会获得该 Mutex,ReleaseMutex 的函数签名如下:

    BOOL ReleaseMutex(HANDLE hMutex);
    
    1

    参数 hMutex 即需要释放所有权的 Mutex 对象句柄。

    我们来看一段具体的实例代码:

    #include <Windows.h>
    #include <string>
    #include <iostream>
    
    HANDLE      g_hMutex = NULL;
    int         g_iResource = 0;
    
    DWORD __stdcall WorkerThreadProc(LPVOID lpThreadParameter)
    {
        DWORD dwThreadID = GetCurrentThreadId();
        while (true)
        {
            if (WaitForSingleObject(g_hMutex, 1000) == WAIT_OBJECT_0)
            {
                g_iResource++;
                std::cout << "Thread: " << dwThreadID << " becomes mutex owner, ResourceNo: " << g_iResource  << std::endl;
                ReleaseMutex(g_hMutex);
            }
            Sleep(1000);
        }
        
        return 0;
    }
    
    int main()
    {
        //创建一个匿名的Mutex对象并设置默认情况下主线程不拥有该Mutex
        g_hMutex = CreateMutex(NULL, FALSE, NULL);
        
        HANDLE hWorkerThreads[5]; 
        for (int i = 0; i < 5; ++i)
        {
            hWorkerThreads[i] = CreateThread(NULL, 0, WorkerThreadProc, NULL, 0, NULL);
        }
    
        for (int i = 0; i < 5; ++i)
        {
            //等待工作线程退出
            WaitForSingleObject(hWorkerThreads[i], INFINITE);
            CloseHandle(hWorkerThreads[i]);
        }
        
        CloseHandle(g_hMutex);
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45

    上述代码中,主线程创建一个 Mutex,并且设置不拥有它,然后五个工作线程去竞争获得这个 Mutex 的使用权,拿到这个 Mutex 之后就可以操作共享资源 g_iResource 了,程序的执行效果是五个工作线程随机获得该资源的使用权:

    互斥体对象的排他性,有点类似于公共汽车上的座位,如果一个座位已经被别人占用,其他人需要等待,如果该座位没人坐,则其他人“先到先得”。当你不需要使用的时候,你需要把座位腾出来让其他需要的人使用。假设某个线程在退出后,仍然没有释放其持有的 Mutex 对象,这时候使用 WaitForSingleObject 等待该 Mutex 对象的线程,也会立即返回,返回值是 WAIT_ABANDONED,表示该 Mutex 处于废弃状态(abandoned),处于废弃状态的 Mutex 不能再使用,其行为是未定义的。

    # 3.6.5 Windows Semaphore 对象

    Semaphore 也是 Windows 多线程同步常用的对象之一,与上面介绍的 Event、Mutex 不同,信号量存在一个资源计数的概念,Event 对象虽然可以同时唤醒多个线程,但是它不能精确地控制同时唤醒指定数目的线程,而 Semaphore 可以。创建 Semaphore 对象的 API 函数签名如下:

    HANDLE CreateSemaphore(
          LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
          LONG                  lInitialCount,
          LONG                  lMaximumCount,
          LPCTSTR               lpName
    );
    
    1
    2
    3
    4
    5
    6

    参数和返回值介绍:

    • 参数 lpSemaphoreAttributes 指定了 Semaphore 对象的安全属性,一般设置为 NULL 使用默认安全属性;
    • 参数 lInitialCount 指定初始可用资源数量,假设初始资源数量为 2,如果有 5 个线程正在调用 WaitForSingleObject 函数等待该信号量,则有 2 个线程会被唤醒,每调用一次 WaitForSingleObject 获得 Semaphore 对象,该对象的资源计数会减少一个。
    • 参数 lMaximumCount 最大资源数量上限,如果使用 ReleaseSemaphore 不断增加资源计数,资源数量最大不能超过这个值,这个值必须设置大于 0。
    • 参数 lpName 指定 Semaphore 对象的名称,Semaphore 对象也是可以通过名称跨进程共享的,如果不需要设置名称可以将该参数设置为 NULL,设置了名称的 Semaphore 对象被称为具名信号量,反之叫匿名信号量。
    • 返回值:函数调用成功返回 Semaphore 对象的句柄,反之返回 NULL。

    如果需要增加信号量的资源计数个数,可以使用 ReleaseSemaphore 函数,其签名如下:

    BOOL ReleaseSemaphore(
          HANDLE hSemaphore,
          LONG   lReleaseCount,
          LPLONG lpPreviousCount
    );
    
    1
    2
    3
    4
    5
    • 参数 hSemaphore 是需要操作的信号量句柄;
    • 参数 lReleaseCount,需要增加的资源数量;
    • 参数 lpPreviousCount 是一个 long 型(32 位系统上 4 个字节)的指针,函数执行成功后,返回上一次资源的数量,如果用不到该参数,可以设置为 NULL。

    Windows的信号量与Linux的信号量用法基本相同,我们来看一个具体的例子。

    假设现在有一个即时通讯的程序,网络线程不断从网络上收到一条条聊天消息,其他 4 个消息处理线程需要对收到的聊天信息进行加工。由于我们需要根据当前消息的数量来唤醒其中 4 个工作线程中的一个或多个,这正是信号量使用的典型案例,代码如下:

    #include <Windows.h>
    #include <string>
    #include <iostream>
    #include <list>
    #include <time.h>
    
    HANDLE                  g_hMsgSemaphore = NULL;
    std::list<std::string>  g_listChatMsg;
    //保护g_listChatMsg的临界区对象
    CRITICAL_SECTION        g_csMsg;
    
    DWORD __stdcall NetThreadProc(LPVOID lpThreadParameter)
    {
        int nMsgIndex = 0;
        while (true)
        {
            EnterCriticalSection(&g_csMsg);
            //随机产生1~4条消息
            int count = rand() % 4 + 1;
            for (int i = 0; i < count; ++i)
            {
                nMsgIndex++;
                SYSTEMTIME st;
                GetLocalTime(&st);
                char szChatMsg[64] = { 0 };
                sprintf_s(szChatMsg, 64, "[%04d-%02d-%02d %02d:%02d:%02d:%03d] A new msg, NO.%d.",
                    st.wYear,
                    st.wMonth,
                    st.wDay,
                    st.wHour,
                    st.wMinute,
                    st.wSecond,
                    st.wMilliseconds,
                    nMsgIndex);
                g_listChatMsg.emplace_back(szChatMsg);
            }   
            LeaveCriticalSection(&g_csMsg);
    
            //增加count个资源数量
            ReleaseSemaphore(g_hMsgSemaphore, count, NULL);
        }// end while-loop
       
        return 0;
    }
    
    DWORD __stdcall ParseThreadProc(LPVOID lpThreadParameter)
    {
        DWORD dwThreadID = GetCurrentThreadId();
        std::string current;
        while (true)
        {
            if (WaitForSingleObject(g_hMsgSemaphore, INFINITE) == WAIT_OBJECT_0)
            {
                EnterCriticalSection(&g_csMsg);
                if (!g_listChatMsg.empty())
                {
                    current = g_listChatMsg.front();
                    g_listChatMsg.pop_front();
                    std::cout << "Thread: " << dwThreadID << " parse msg: " << current << std::endl;
                }         
                LeaveCriticalSection(&g_csMsg);
            }
        }
    
        return 0;
    }
    
    int main()
    {
        //初始化随机数种子
        srand(time(NULL));
        InitializeCriticalSection(&g_csMsg);
        
        //创建一个匿名的Semaphore对象,初始资源数量为0
        g_hMsgSemaphore = CreateSemaphore(NULL, 0, INT_MAX, NULL);
    
        HANDLE hNetThread = CreateThread(NULL, 0, NetThreadProc, NULL, 0, NULL);
    
        HANDLE hWorkerThreads[4];
        for (int i = 0; i < 4; ++i)
        {
            hWorkerThreads[i] = CreateThread(NULL, 0, ParseThreadProc, NULL, 0, NULL);
        }
    
        for (int i = 0; i < 4; ++i)
        {
            //等待工作线程退出
            WaitForSingleObject(hWorkerThreads[i], INFINITE);
            CloseHandle(hWorkerThreads[i]);
        }
    
        WaitForSingleObject(hNetThread, INFINITE);
        CloseHandle(hNetThread);
    
        CloseHandle(g_hMsgSemaphore);
    
        DeleteCriticalSection(&g_csMsg);
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99

    上述代码中,网络线程每次随机产生 1 ~ 4 个聊天消息放入消息容器 g_listChatMsg 中,然后根据当前新产生的消息数目调用 ReleaseSemaphore 增加相应的资源计数,这样就有相应的处理线程被唤醒,从容器 g_listChatMsg 中取出消息进行处理。

    注意:由于会涉及到多个线程操作消息容器 g_listChatMsg,我们这里使用了一个临界区对象 g_csMsg 对其进行保护。

    程序执行效果如下:

    //这里截取输出中间部分...输出太多,部分结果省略
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.26.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.27.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.28.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.29.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.30.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.31.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.32.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.33.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.34.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.35.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.36.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.37.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.38.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.39.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.40.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.41.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.42.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:568] A new msg, NO.43.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:569] A new msg, NO.44.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:569] A new msg, NO.45.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:569] A new msg, NO.46.
    Thread: 3704 parse msg: [2019-01-20 16:31:47:569] A new msg, NO.47.
    Thread: 5512 parse msg: [2019-01-20 16:31:47:569] A new msg, NO.48.
    Thread: 6676 parse msg: [2019-01-20 16:31:47:569] A new msg, NO.49.
    Thread: 6676 parse msg: [2019-01-20 16:31:47:569] A new msg, NO.50.
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    # 3.6.6 Windows 读写锁

    与 Linux 的读写相同的原理,Windows 系统也有读写锁。Windows 系统上的读写锁叫 Slim Reader/Writer (SRW) Locks,对应的数据类型叫 SRWLOCK,微软也没有公开这个数据结构的细节,只提供了一些 API 函数对其操作之。

    //初始化一个读写锁,PSRWLOCK 的定义是 SRWLOCK*
    void InitializeSRWLock(PSRWLOCK SRWLock);
    
    //以共享模式获得读写锁
    void AcquireSRWLockShared(PSRWLOCK SRWLock);
    //释放共享模式的读写锁
    void ReleaseSRWLockShared(PSRWLOCK SRWLock);
    
    //以排他模式获得读写锁
    void AcquireSRWLockExclusive(PSRWLOCK SRWLock);
    //释放排他模式的读写锁
    void ReleaseSRWLockExclusive(PSRWLOCK SRWLock);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    还可以使用如下语法初始化一个 SRWLOCK 对象:

    SRWLOCK mySRWLock = SRWLOCK_INIT;
    
    1

    与临界区对象不同的是, Windows 不需要显式销毁一个读写锁对象,因此不存在 DeleteSRWLock 这样的函数用于销毁一个读写锁。

    综合前面介绍的 Linux 的读写锁,以下是我对 Windows 和 Linux 平台的读写锁对象进行简单封装的一个工具类,用于模拟 C++17 的 std::shared_mutex(下文中介绍):

    SharedMutex.h

    /** 
     * SharedMutex.h C++11 没有std::shared_mutex, 自己实现一个
     * zhangyl 20191108
     */
    
    #ifndef __SHARED_MUTEX_H__
    #define __SHARED_MUTEX_H__
    
    #ifdef WIN32
    #include <Windows.h>
    #else
    #include 
    #include <pthread.h>
    #endif
    
    //模拟std::shared_mutex
    class SharedMutex final
    {
    public:
        SharedMutex();
        ~SharedMutex();
    
        void acquireReadLock();
        void acquireWriteLock();
        void unlockReadLock();
        void unlockWriteLock();
    
    private:
        SharedMutex(const SharedMutex& rhs) = delete;
        SharedMutex& operator =(const SharedMutex& rhs) = delete;
    
    private:
    #ifdef WIN32
        SRWLOCK             m_SRWLock;
    #else
        pthread_rwlock_t    m_SRWLock;
    #endif   
    };
    
    //模拟std::shared_lock
    class SharedLockGuard final
    {
    public:
        SharedLockGuard(SharedMutex& sharedMutex);
        ~SharedLockGuard();
    
    private:
        SharedLockGuard(const SharedLockGuard& rhs) = delete;
        SharedLockGuard operator=(const SharedLockGuard& rhs) = delete;
    
    private:
        SharedMutex&        m_SharedMutex;
    };
    
    //模拟std::unique_lock
    class UniqueLockGuard final
    {
    public:
        UniqueLockGuard(SharedMutex& sharedMutex);
        ~UniqueLockGuard();
    
    private:
        UniqueLockGuard(const UniqueLockGuard& rhs) = delete;
        UniqueLockGuard operator=(const UniqueLockGuard& rhs) = delete;
    
    private:
        SharedMutex& m_SharedMutex;
    };
    
    #endif //!__SHARED_MUTEX_H__
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70

    SharedMutex.cpp

    /**
     * SharedMutex.cpp
     * zhangyl 20191108
     */
    
    #include "SharedMutex.h"
    
    SharedMutex::SharedMutex()
    {
    #ifdef WIN32
        ::InitializeSRWLock(&m_SRWLock);
    #else
        ::pthread_rwlock_init(&m_SRWLock, nullptr);
    #endif   
    }
    
    SharedMutex::~SharedMutex()
    {
    #ifdef WIN32
        //Windows上读写锁不需要显式销毁
    #else
        ::pthread_rwlock_destroy(&m_SRWLock);
    #endif
    }
    
    void SharedMutex::acquireReadLock()
    {
    #ifdef WIN32
        ::AcquireSRWLockShared(&m_SRWLock);
    #else
        ::pthread_rwlock_rdlock(&m_SRWLock);
    #endif   
    }
    
    void SharedMutex::acquireWriteLock()
    {
    #ifdef WIN32
        ::AcquireSRWLockExclusive(&m_SRWLock);
    #else
        ::pthread_rwlock_wrlock(&m_SRWLock);
    #endif   
    }
    
    void SharedMutex::unlockReadLock()
    {
    #ifdef WIN32   
        ::ReleaseSRWLockShared(&m_SRWLock);   
    #else
        ::pthread_rwlock_unlock(&m_SRWLock);
    #endif
    }
    
    void SharedMutex::unlockWriteLock()
    {
    #ifdef WIN32  
        ::ReleaseSRWLockExclusive(&m_SRWLock);
    #else
        ::pthread_rwlock_unlock(&m_SRWLock);
    #endif
    }
    
    SharedLockGuard::SharedLockGuard(SharedMutex& sharedMutex) :
        m_SharedMutex(sharedMutex)
    {
        m_SharedMutex.acquireReadLock();
    }
    
    SharedLockGuard::~SharedLockGuard()
    {
        m_SharedMutex.unlockReadLock();
    }
    
    UniqueLockGuard::UniqueLockGuard(SharedMutex& sharedMutex) :
        m_SharedMutex(sharedMutex)
    {
        m_SharedMutex.acquireWriteLock();
    }
    
    UniqueLockGuard::~UniqueLockGuard()
    {
        m_SharedMutex.unlockWriteLock();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82

    # 3.6.7 Windows 条件变量

    和 Linux 的条件变量作用一样,Windows 系统在Windows XP 和 Windows Server 2003 版本以后也引入了条件变量(言下之意,Windows XP 及以下版本不支持条件变量)。在 Windows 中代表条件变量的结构体对象是 CONDITION_VARIABLE,微软并没有给出这个结构的具体定义,只提供了一系列 API 函数去操作这个对象。

    初始化一个 Windows 条件变量使用如下 API 函数:

    //PCONDITION_VARIABLE 类型是 CONDITION_VARIABLE*,
    //即结构体 CONDITION_VARIABLE 的指针类型
    void InitializeConditionVariable(PCONDITION_VARIABLE ConditionVariable);
    
    1
    2
    3

    你也可以使用如下语法初始化一个条件变量对象:

    CONDITION_VARIABLE myConditionVariable = CONDITION_VARIABLE_INIT;
    
    1

    与临界区(CriticalSection)、读写锁(Slim Reader/Writer Locks)一样,Windows 的条件变量也是 user-mode 对象,不可以跨进程共享。

    Windows 上使用条件变量需要配合一个临界区或读写锁,使用临界区的方式和 Linux 的条件变量类似,等待资源变为可用的线程调用 SleepConditionVariableCS() 或 SleepConditionVariableSRW 函数进行等待,这两个函数的签名如下:

    BOOL SleepConditionVariableCS(
      PCONDITION_VARIABLE ConditionVariable,
      PCRITICAL_SECTION   CriticalSection,
      DWORD               dwMilliseconds
    );
    
    BOOL SleepConditionVariableSRW(
      PCONDITION_VARIABLE ConditionVariable,
      PSRWLOCK            SRWLock,
      DWORD               dwMilliseconds,
      ULONG               Flags
    );
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    这两个函数分别使用了一个临界区(CriticalSection 参数)和一个读写锁( SRWLock 参数),参数 dwMilliseconds 可用于设置一个等待时间,如果需要无限等待可以设置值为 INFINITE,这两个函数在 dwMilliseconds 超时时间内阻塞当前调用线程。函数调用成功返回非 0 值,调用失败返回 0 值。如果是由于 dwMilliseconds 超时而造成的调用失败,调用 GetLastError() 会得到错误码 ERROR_TIMEOUT。

    和 Linux 条件变量使用方式一样,在调用 SleepConditionVariableCS 或者 SleepConditionVariableSRW 函数之前,调用线程必须“持有”对应的临界区或读写锁对象;在调用 SleepConditionVariableCS 或 SleepConditionVariableSRW 函数之后,线程进入睡眠后,会释放“持有”的临界区或读写锁对象;SleepConditionVariableCS 或者 SleepConditionVariableSRW 函数成功返回后,将再次“持有”对应的互斥体或读写锁对象,此时资源已经变为可用,在操作完资源后如果希望其他线程可以继续操作资源,需要释放其“持有”的临界区或读写锁对象。

    以上步骤伪码如下(这里以临界区为例):

    CRITICAL_SECTION CritSection;
    CONDITION_VARIABLE ConditionVar;
    
    void PerformOperationOnSharedResource()
    { 
       //进入临界区,持有“临界区”锁
       EnterCriticalSection(&CritSection);
    
       // 等待共享资源变为可用
       while( TestSharedResourceAvailable() == FALSE )
       {
          //线程无限等待,直到资源可通
          SleepConditionVariableCS(&ConditionVar, &CritSection, INFINITE);
       }
       
       //资源这里已经变为可用,其进行操作
       OperateSharedResource();
    
       //离开临界区对象,让其他线程可以对共享资源进行操作
       LeaveCriticalSection(&CritSection);
    
       //其他一些操作
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    等待资源变为可用的线程在调用 SleepConditionVariableCS 或者 SleepConditionVariableSRW 函数后,释放其“持有”的临界区或读写锁对象后,其他线程就有机会对共享资源进行修改了,这些线程修改了共享资源后让资源变为可用,可以调用 WakeConditionVariable 或 WakeAllConditionVariable 唤醒调用 SleepConditionVariableCS 或者 SleepConditionVariableSRW 函数等待的线程,前者只唤醒一个等待的线程,后者唤醒所有等待的线程。这两个函数的签名如下:

    //唤醒单个线程
    void WakeConditionVariable(PCONDITION_VARIABLE ConditionVariable);
    
    //唤醒多个线程
    void WakeAllConditionVariable(PCONDITION_VARIABLE ConditionVariable);
    
    1
    2
    3
    4
    5

    需要注意的是,和 Linux 的条件变量一样,Windows 的条件变量也存在虚假唤醒这一行为,所以当条件变量被唤醒时,不一定是其他线程调用 WakeConditionVariable 或 WakeAllConditionVariable 唤醒的,而是操作系统造成的虚假唤醒。所以上述伪代码中,即使 SleepConditionVariableCS 函数返回了,还需要再次判断一下资源是否可用,所以放在一个 while 循环里面判断资源是否可用。

    // 等待共享资源变为可用
    while( TestSharedResourceAvailable() == FALSE )
    {
        //线程无限等待,直到资源可通
        //SleepConditionVariableCS 返回后会再次对 while 条件进行判断,
        //如果是虚假唤醒,while 条件仍然是 FALSE,线程再次调用 SleepConditionVariableCS 进行等待
        SleepConditionVariableCS(&ConditionVar, &CritSection, INFINITE);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

    我们将 3.5 节的条件变量的例子改写成 Windows 版本:

    /** 
     * 演示 Windows 条件变量的使用
     * zhangyl 20191111
     */
    
    #include <Windows.h>
    #include <iostream>
    #include <list>
    
    class Task
    {
    public:
        Task(int taskID)
        {
            this->taskID = taskID;
        }
    
        void doTask()
        {
            std::cout << "handle a task, taskID: " << taskID << ", threadID: " << GetCurrentThreadId() << std::endl;
        }
    
    private:
        int taskID;
    };
    
    CRITICAL_SECTION    myCriticalSection;
    CONDITION_VARIABLE  myConditionVar;
    std::list<Task*>    tasks;
    
    DWORD WINAPI consumerThread(LPVOID param)
    {
        Task* pTask = NULL;
        while (true)
        {
            //进入临界区
            EnterCriticalSection(&myCriticalSection);
            while (tasks.empty())
            {
                //如果SleepConditionVariableCS挂起,挂起前会离开临界区,不往下执行。
                //当发生变化后,条件合适,SleepConditionVariableCS将直接进入临界区。
                SleepConditionVariableCS(&myConditionVar, &myCriticalSection, INFINITE);
            }
    
            pTask = tasks.front();
            tasks.pop_front();
    
            //SleepConditionVariableCS被唤醒后进入临界区,
            //为了让其他线程有机会操作tasks,这里需要再次离开临界区
            LeaveCriticalSection(&myCriticalSection);
    
            if (pTask == NULL)
                continue;
    
            pTask->doTask();
            delete pTask;
            pTask = NULL;
        }
    
        return NULL;
    }
    
    DWORD WINAPI producerThread(LPVOID param)
    {
        int taskID = 0;
        Task* pTask = NULL;
    
        while (true)
        {
            pTask = new Task(taskID);
    
            //进入临界区
            EnterCriticalSection(&myCriticalSection);
            tasks.push_back(pTask);
            std::cout << "produce a task, taskID: " << taskID << ", threadID: " << GetCurrentThreadId() << std::endl;
    
            LeaveCriticalSection(&myCriticalSection);
    
            WakeConditionVariable(&myConditionVar);
    
            taskID++;
    
            //休眠1秒
            Sleep(1000);
        }
    
        return NULL;
    }
    
    int main()
    {
        InitializeCriticalSection(&myCriticalSection);
        InitializeConditionVariable(&myConditionVar);
    
        //创建5个消费者线程
        HANDLE consumerThreadHandles[5];
        for (int i = 0; i < 5; ++i)
        {
            consumerThreadHandles[i] = CreateThread(NULL, 0, consumerThread, NULL, 0, NULL);
        }
    
        //创建一个生产者线程
        HANDLE producerThreadHandle = CreateThread(NULL, 0, producerThread, NULL, 0, NULL);
    
        //等待生产者线程退出
        WaitForSingleObject(producerThreadHandle, INFINITE);
    
        //等待消费者线程退出
        for (int i = 0; i < 5; ++i)
        {
            WaitForSingleObject(consumerThreadHandles[i], INFINITE);
        }
    
        DeleteCriticalSection(&myCriticalSection);
    
        return 0;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117

    上述代码创建了 1 个生产者线程和 5 个消费者线程,每当生产者产生一个 task 后放入队列中,并随机唤醒一个消费者线程,因此每次处理任务的消费者线程的 ID 是不一样的,程序执行结果如下:

    # 3.6.8 多进程之间共享线程同步对象

    前面介绍的 Windows Event、Mutex、Semaphore 对象其创建函数 CreateX 都可以给这些对象指定一个名字,有了名字之后这些线程资源同步对象就可以通过这个名字在不同进程之间共享。

    在 Windows 系统上读者应该有这样的体验:有些程序无论双击其启动图标几次都只会启动一个实例,我们把这类程序叫做单实例程序(Single Instance)。我们可以利用命名的线程资源同步对象来实现这个效果,这里以互斥体为例。

    示例代码如下:

    int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
                         _In_opt_ HINSTANCE hPrevInstance,
                         _In_ LPTSTR    lpCmdLine,
                         _In_ int       nCmdShow)
    {
    	//...省略无关代码...
    
        if (CheckInstance())
        {
            HWND hwndPre = FindWindow(szWindowClass, NULL);
            if (IsWindow(hwndPre))
            {
                if (::IsIconic(hwndPre))
                    ::SendMessage(hwndPre, WM_SYSCOMMAND, SC_RESTORE | HTCAPTION, 0);
    
                ::SetWindowPos(hwndPre, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_NOACTIVATE);
                ::SetForegroundWindow(hwndPre);
                ::SetFocus(hwndPre);
                return 0;
            }
        }
        
        //...省略无关代码
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    上述代码在 WinMain 函数开始处先检查是否已经存在运行起来的程序实例,如果存在,则找到运行中的实例程序主窗口并激活之,这就是读者看到最小化很多单例程序后双击该程序图标会重新激活最小化的程序的效果实现原理。

    现在重点是 CheckInstance() 函数的实现:

    bool CheckInstance()
    {
        HANDLE hSingleInstanceMutex = CreateMutex(NULL, FALSE, _T("MySingleInstanceApp"));
        if (hSingleInstanceMutex != NULL)
        {
            if (GetLastError() == ERROR_ALREADY_EXISTS)
            {
                return true;
            }
        }
    
        return false;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    假设首次启动这个进程,这个进程会调用 CreateMutex 函数创建一个名称为“MySingleInstanceApp”的互斥体对象。当再次准备启动一份这个进程时,再次调用 CreateMutex 函数,由于该名称的互斥体对象已经存在,将会返回已经存在的互斥体对象地址,此时通过 GetLastError() 函数得到的错误码是 ERROR_ALREADY_EXISTS 表示该名称的互斥体对象已经存在,此时我们激活已经存在的前一个实例,然后退出当前进程即可。

    上次更新: 2025/05/07, 21:40:50
    3.5 Linux线程同步对象
    3.7 C++ 11/14/17 线程同步对象

    ← 3.5 Linux线程同步对象 3.7 C++ 11/14/17 线程同步对象→

    最近更新
    01
    第二章 关键字static及其不同用法
    03-27
    02
    第一章 auto与类型推导
    03-27
    03
    C++语言面试问题集锦 目录与说明
    03-27
    更多文章>
    Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式