Windows 平台 C++ 并发编程之一

Windows 临界区

Windows 临界区的概述

Windows 临界区(CRITICAL_SECTION)是 Windows 系统提供的一种线程同步机制,主要用于保护进程内多个线程访问共享数据,避免多个线程同时访问共享资源而产生数据竞争。

  • 核心特点
    • 进程内使用:只能用于同一个进程中的线程之间进行同步。
    • 互斥访问:同一时刻通常只有一个线程能够进入临界区,其他线程需要等待。
    • 轻量级:相比 Windows 内核互斥对象,临界区通常具有更低的开销,适合频繁进行线程同步的场景。
    • 需要初始化和销毁:使用前需要初始化,使用结束后需要释放相关资源。
    • 手动加锁和解锁:进入临界区后必须确保最终能够离开,否则可能导致其他线程长期等待甚至死锁。
    • 不支持跨进程同步:如果需要实现不同进程之间的同步,应使用 Windows 的互斥体(Mutex)等内核同步对象。
  • std::mutex 的关系

    • 从功能上看,Windows 系统的 CRITICAL_SECTION 与 C++ 标准库的 std::mutex 都可以用于实现线程之间的互斥访问。
    • 主要区别在于:CRITICAL_SECTION 是 Windows 平台相关的 API,而 std::mutex 是 C++ 标准库提供的跨平台同步工具。
    • 因此,在 Windows 专用程序中可以使用 CRITICAL_SECTION;如果希望代码具有更好的跨平台能力,通常更推荐使用 std::mutex
    • 注意:
      • CRITICAL_SECTION 支持同一个线程多次进入同一个 Windows 临界区,属于递归锁(可重入锁)。
      • 但是,std::mutex 不支持递归加锁,同一线程多次调用 lock() 会导致阻塞,因此不具备可重入性
      • 在 C++ 11 中,如果需要支持同一线程重复加锁,应该使用 std::recursive_mutex 或者 std::recursive_timed_mutex
  • 多次进入同一个临界区

    • 同一个线程可以多次进入同一个 Windows 临界区,这里类似于 Java 的可重入锁(synchronizedReentrantLock 等)
    • Windows 临界区支持递归进入:同一个线程在已经进入临界区的情况下,可以再次调用进入临界区操作,不会发生阻塞
    • 内部通过 “递归计数” 管理:Windows 临界区会记录同一线程进入临界区的次数。
    • 进入与离开需要匹配:调用多少次进入临界区操作,就需要调用相同次数的离开临界区操作,直到计数归零后,临界区才真正释放。
    • 注意:如果进入次数与离开次数不匹配,可能导致其他线程长期无法进入临界区,造成线程死锁问题。
    • 举例:
      1
      2
      3
      4
      5
      EnterCriticalSection(&winsec);  // 第一次进入临界区(加锁)
      EnterCriticalSection(&winsec); // 第二次进入临界区(加锁)
      msgRecvQueue.push_back(i);
      LeaveCriticalSection(&winsec); // 第一次离开临界区(解锁)
      LeaveCriticalSection(&winsec); // 第二次离开临界区(解锁)

总结

Windows 临界区是一种轻量级的进程内线程同步机制,用于保证同一时刻只有一个线程可以访问受保护的共享资源。它性能较好,但属于 Windows 系统专用 API,现代 C++ 开发中通常推荐优先考虑使用 std::mutex(互斥量)。特别注意,Windows 的 CRITICAL_SECTIONstd::mutex 的一个重要区别:Windows 临界区允许同一线程递归进入(即可重入),而 std::mutex 不允许

Windows 临界区的使用

临界区使用案例一

案例背景说明

在网络游戏服务器的设计中,共享数据的保护是一个典型案例:可以创建两个线程,其中一个线程负责收集玩家的命令并将命令数据写入队列,另一个线程则从队列中取出玩家发来的命令,进行解析并执行玩家所需的动作。值得一提的是,在当前业务场景下建议使用生产者消费者模型来实现,并使用 list 容器作为队列。

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#include <atomic>
#include <chrono>
#include <iostream>
#include <list>
#include <mutex>
#include <thread>
#include <Windows.h>

#define WINDOWS_CRITICAL_SECTION // 标记 Windows 系统环境

class MyClass {
public:
// 将收到的玩家命令写入队列
void inMsgRecvQueue() {
for (int i = 0; i < 1000; ++i) {
#ifdef WINDOWS_CRITICAL_SECTION
// 加锁
EnterCriticalSection(&winsec);

// 插入队列
msgRecvQueue.push_back(i);

// 解锁
LeaveCriticalSection(&winsec);
#else
{
// 加锁(出了作用域后会自动解锁)
std::unique_lock<std::mutex> lock(msgRecvQueueMutex);

// 插入队列
msgRecvQueue.push_back(i);
}
#endif

// 模拟网络收包间隔
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}

// 更新程序停止标记
stop = true;
}

// 从队列中读取玩家命令
void outMsgRecvQueue() {
while (true) {
int command = -1;

#ifdef WINDOWS_CRITICAL_SECTION
// 加锁
EnterCriticalSection(&winsec);

// 判断程序停止标记
if (stop && msgRecvQueue.empty()) {
// 解锁
LeaveCriticalSection(&winsec);
break;
}

// 操作队列
if (!msgRecvQueue.empty()) {
// 取出队列元素
command = msgRecvQueue.front();
// 移除队列元素
msgRecvQueue.pop_front();
}

// 解锁
LeaveCriticalSection(&winsec);
#else
{
// 加锁(出了作用域后会自动解锁)
std::unique_lock<std::mutex> lock(msgRecvQueueMutex);

// 判断程序停止标记
if (stop && msgRecvQueue.empty()) {
break;
}

// 操作队列
if (!msgRecvQueue.empty()) {
// 取出队列元素
command = msgRecvQueue.front();
// 移除队列元素
msgRecvQueue.pop_front();
}
}
#endif

// 打印
if (command != -1) {
std::cout << "已处理玩家命令: " << command << std::endl;
}

// 模拟业务执行耗时
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}

// 构造函数
MyClass() {
#ifdef WINDOWS_CRITICAL_SECTION
InitializeCriticalSection(&winsec); // 初始化临界区
#endif
}

// 析构函数
~MyClass() {
#ifdef WINDOWS_CRITICAL_SECTION
DeleteCriticalSection(&winsec); // 删除临界区
#endif
}

private:
std::list<int> msgRecvQueue; // 消息队列(共享数据)
std::mutex msgRecvQueueMutex; // 保护消息队列线程安全的互斥锁
std::atomic_bool stop{false}; // 程序停止标记

#ifdef WINDOWS_CRITICAL_SECTION
CRITICAL_SECTION winsec; // Windows 系统中的临界区,作用非常类似于 C++ 11 中的 std::mutex
#endif

};

int main() {
std::cout << "main thread start." << std::endl;

// 局部变量
MyClass mc;

// 创建并启动写线程
std::thread t_write(&MyClass::inMsgRecvQueue, &mc);

// 创建并启动读线程
std::thread t_read(&MyClass::outMsgRecvQueue, &mc);

// 等待写线程执行完毕
t_write.join();

// 等待读线程执行完毕
t_read.join();

std::cout << "main thread end." << std::endl;
return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
已处理玩家命令: 0
已处理玩家命令: 1
已处理玩家命令: 2
已处理玩家命令: 3
......
已处理玩家命令: 996
已处理玩家命令: 997
已处理玩家命令: 998
已处理玩家命令: 999

特别注意

Windows 系统的 CRITICAL_SECTION 本身不像 std::lock_guard 那样可以自动管理锁,更类似于 std::mutex,所以使用时加锁后千万不要忘记解锁

临界区使用案例二

Windows 系统的 CRITICAL_SECTION 不支持自动离开临界区(自动解锁),为了实现 std::lock_guard 自动解锁的功能,可以使用以下方式封装一个自定义的 RAII 类(比如 CWinLock)。值得注意的是,CWinLock 支持递归锁(可重入锁)。CWinLock 基于 Windows CRITICAL_SECTION 实现,而 CRITICAL_SECTION 支持同一线程递归进入(可重入)。因此,同一线程可以递归获取同一个 CWinLock,每次进入都会对应一次离开;只有进入次数降为 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#include <atomic>
#include <chrono>
#include <iostream>
#include <list>
#include <mutex>
#include <thread>
#include <windows.h>

#define WINDOWS_CRITICAL_SECTION // 标记 Windows 系统环境

#ifdef WINDOWS_CRITICAL_SECTION
// RAII 类,用于自动释放 Windows 的临界区,防止忘记释放临界区导致线程死锁问题的发生,类似于 C++ 11 中的 std::lock_guard
class CWinLock {
public:
// 构造函数
CWinLock(CRITICAL_SECTION * pCritical) : m_pCritical(pCritical) {
EnterCriticalSection(pCritical); // 进入临界区
}

// 析构函数
~CWinLock() {
LeaveCriticalSection(m_pCritical); // 释放临界区
}

// 禁止拷贝构造
CWinLock(const CWinLock &lock) = delete;

// 禁止拷贝赋值
CWinLock& operator=(const CWinLock &lock) = delete;

private:
CRITICAL_SECTION* const m_pCritical; // Windows 临界区
};
#endif

class MyClass {
public:
// 将收到的玩家命令写入队列
void inMsgRecvQueue() {
for (int i = 0; i < 1000; ++i) {
#ifdef WINDOWS_CRITICAL_SECTION
{
// 加锁(出了作用域会自动解锁)
CWinLock lock(&winsec);

// 插入队列
msgRecvQueue.push_back(i);
}
#else
{
// 加锁(出了作用域后会自动解锁)
std::unique_lock<std::mutex> lock(msgRecvQueueMutex);

// 插入队列
msgRecvQueue.push_back(i);
}
#endif

// 模拟网络收包间隔
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}

// 更新程序停止标记
stop = true;
}

// 从队列中读取玩家命令
void outMsgRecvQueue() {
while (true) {
int command = -1;

#ifdef WINDOWS_CRITICAL_SECTION
{
// 加锁(出了作用域会自动解锁)
CWinLock lock(&winsec);

// 判断程序停止标记
if (stop && msgRecvQueue.empty()) {
break;
}

// 操作队列
if (!msgRecvQueue.empty()) {
// 取出队列元素
command = msgRecvQueue.front();
// 移除队列元素
msgRecvQueue.pop_front();
}
}
#else
{
// 加锁(出了作用域后会自动解锁)
std::unique_lock<std::mutex> lock(msgRecvQueueMutex);

// 判断程序停止标记
if (stop && msgRecvQueue.empty()) {
break;
}

// 操作队列
if (!msgRecvQueue.empty()) {
// 取出队列元素
command = msgRecvQueue.front();
// 移除队列元素
msgRecvQueue.pop_front();
}
}
#endif

// 打印
if (command != -1) {
std::cout << "已处理玩家命令: " << command << std::endl;
}

// 模拟业务执行耗时
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}

// 构造函数
MyClass() {
#ifdef WINDOWS_CRITICAL_SECTION
InitializeCriticalSection(&winsec); // 初始化临界区
#endif
}

// 析构函数
~MyClass() {
#ifdef WINDOWS_CRITICAL_SECTION
DeleteCriticalSection(&winsec); // 删除临界区
#endif
}

private:
std::list<int> msgRecvQueue; // 消息队列(共享数据)
std::mutex msgRecvQueueMutex; // 保护消息队列线程安全的互斥锁
std::atomic_bool stop{false}; // 程序停止标记

#ifdef WINDOWS_CRITICAL_SECTION
CRITICAL_SECTION winsec; // Windows 系统中的临界区,作用非常类似于 C++ 11 中的 std::mutex
#endif

};

int main() {
std::cout << "main thread start." << std::endl;

// 局部变量
MyClass mc;

// 创建并启动写线程
std::thread t_write(&MyClass::inMsgRecvQueue, &mc);

// 创建并启动读线程
std::thread t_read(&MyClass::outMsgRecvQueue, &mc);

// 等待写线程执行完毕
t_write.join();

// 等待读线程执行完毕
t_read.join();

std::cout << "main thread end." << std::endl;
return 0;
}

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
已处理玩家命令: 0
已处理玩家命令: 1
已处理玩家命令: 2
已处理玩家命令: 3
......
已处理玩家命令: 996
已处理玩家命令: 997
已处理玩家命令: 998
已处理玩家命令: 999

总结

通过 RAII 封装 CRITICAL_SECTION,在构造时自动进入临界区,在析构时自动释放临界区,从而避免因遗漏 LeaveCriticalSection() 而导致线程死锁。CRITICAL_SECTION 负责临界区本身的生命周期,CWinLock 负责一次加锁 / 解锁操作的 RAII 管理。值得注意的是,不建议在 CWinLock 这个 RAII 类中调用 InitializeCriticalSection() / DeleteCriticalSection(),因为 CWinLock 的职责只是管理一次 Enter/Leave,而不是管理 CRITICAL_SECTION 本身的生命周期。