C++ 从入门到精通之十

大纲

C++ 并发编程

互斥量

互斥量的概念

C++ 中的互斥量(mutex)是一种用于保护共享数据免受并发访问影响的同步原语。它的核心机制是 “独占锁定”:同一时刻,只有一个线程能成功锁定(lock)互斥量并进入临界区执行代码,其他试图进入的线程必须等待,直到持有锁的线程调用解锁(unlock)释放所有权。这种机制有效防止了数据竞争(Data Race)和未定义行为。C++ 11 标准库将互斥量纳入了线程支持库,并提供了 std::lock_guardstd::unique_lock 等 RAII 包装器,用于自动管理锁的获取与释放,避免因异常或忘记解锁而导致的死锁问题。

互斥量的使用

lock()、unlock()

互斥量(mutex)的基本用法遵循显式的加锁与解锁配对原则:线程必须先调用 lock() 获取互斥量,然后安全地操作共享数据,最后调用 unlock() 释放所有权。

特别注意

互斥量(mutex)的 lock()unlock() 必须严格成对使用,每次 lock() 对应一次 unlock(),既不能多调用一次 unlock(),也不能少调用一次,否则会导致未定义行为或线程死锁等严重问题。

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
#include <atomic>
#include <iostream>
#include <list>
#include <mutex>
#include <thread>

class MyClass {
public:
// 将收到的玩家命令写入队列
void inMsgRecvQueue() {
for (int i = 0; i < 1000; ++i) {
// 加锁
msgRecvQueueMutex.lock();

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

// 解锁
msgRecvQueueMutex.unlock();

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

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

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

// 加锁
msgRecvQueueMutex.lock();

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

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

// 解锁
msgRecvQueueMutex.unlock();

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

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

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

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
std::lock_guard 模板

C++ 11 标准库将互斥量纳入了线程支持库,并提供了 std::lock_guardstd::unique_lock 等 RAII 包装器,用于自动管理锁的获取与释放,避免因异常或忘记解锁而导致的死锁问题。值得一提的是,std::lock_guard 通过 RAII(资源获取即初始化)机制实现自动加锁和解锁:在构造时,它的构造函数会接收一个互斥量对象并立即调用该互斥量的 lock() 方法完成加锁;在析构时,它的析构函数会自动调用互斥量的 unlock() 方法完成解锁。这样,当 lock_guard 对象被创建时锁就被获取,当该对象离开作用域(无论是正常结束、returnbreak 还是抛出异常)时,析构函数会被自动触发,从而保证锁一定会被释放,避免了因忘记调用 unlock() 或异常导致死锁的问题。

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
#include <atomic>
#include <iostream>
#include <list>
#include <mutex>
#include <thread>

class MyClass {
public:
// 将收到的玩家命令写入队列
void inMsgRecvQueue() {
for (int i = 0; i < 1000; ++i) {
{
// 加锁(出了作用域后会自动解锁)
std::lock_guard<std::mutex> lock(msgRecvQueueMutex);

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

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

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

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

{
// 加锁(出了作用域后会自动解锁)
std::lock_guard<std::mutex> lock(msgRecvQueueMutex);

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

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

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

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

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

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

线程死锁

线程死锁的案例