C++ 从入门到精通之十

大纲

C++ 并发编程

互斥量

互斥量的概念

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

互斥量的使用

lock()、unlock()

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

特别注意

互斥量(std::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

线程死锁

C++ 中的线程死锁是指两个或多个线程在执行过程中,因互相等待对方持有的资源(如互斥锁)而永远阻塞的状态。典型场景是线程 A 持有锁 1 并等待锁 2,而线程 B 持有锁 2 并等待锁 1,双方都无法继续推进。死锁的本质是资源竞争和循环依赖,会导致程序完全停滞,既不会崩溃也不会自动恢复。避免死锁的常见策略包括:按固定顺序获取锁、使用超时机制(如 try_lock())、采用分层锁或使用 RAII 管理锁的持有时间,从而打破循环等待条件。

死锁的四个必要条件代码中的体现
互斥std::mutex 保证同一时间只有一个线程能持有锁
持有并等待线程 1 持有 lock1 等待 lock2;线程 2 持有 lock2 等待 lock1
不可抢占std::mutex 不能被强制释放,只能由持有者主动 unlock(解锁)
循环等待线程 1 → lock1 → lock2,线程 2 → lock2 → lock1,形成环路

触发线程死锁的案例

下面这段代码会触发线程死锁,因为两个线程以相反的顺序获取锁:线程 1 先锁 lock1 再等待 lock2,线程 2 先锁 lock2 再等待 lock1,导致两个线程各自持有一个锁并等待对方释放另一个锁,形成循环等待,结果谁也无法继续执行,程序永久阻塞。

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

class MyClass {
public:
void func1() {
std::cout << "thread 1 wait to get locker 1" << std::endl;

// 等待获取锁 1
lock1.lock();

std::cout << "thread 1 already got locker 1" << std::endl;

// 等待一段时间再获锁 2
std::this_thread::sleep_for(std::chrono::milliseconds(1000));

std::cout << "thread 1 wait to get locker 2" << std::endl;

// 等待获取锁 2
lock2.lock();

std::cout << "thread 1 already got locker 2" << std::endl;

// 释放锁 2
lock2.unlock();

std::cout << "thread 1 already released locker 2" << std::endl;

// 释放锁 1
lock1.unlock();

std::cout << "thread 1 already released locker 1" << std::endl;
}

void func2() {
std::cout << "thread 2 wait to get locker 2" << std::endl;

// 等待获取锁 2
lock2.lock();

std::cout << "thread 2 already got locker 2" << std::endl;

// 等待一段时间再获锁 1
std::this_thread::sleep_for(std::chrono::milliseconds(1000));

std::cout << "thread 2 wait to get locker 1" << std::endl;

// 等待获取锁 1
lock1.lock();

std::cout << "thread 2 already got locker 1" << std::endl;

// 释放锁 1
lock1.unlock();

std::cout << "thread 2 already released locker 1" << std::endl;

// 释放锁 2
lock2.unlock();

std::cout << "thread 2 already released locker 2" << std::endl;
}

private:
std::mutex lock1; // 锁 1
std::mutex lock2; // 锁 2
};

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

MyClass mc;

// 创建并启动线程 1
std::thread t1(&MyClass::func1, &mc);

// 创建并启动线程 2
std::thread t2(&MyClass::func2, &mc);

// 等待线程 1 执行结束
t1.join();

// 等待线程 2 执行结束
t2.join();

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

程序运行的结果如下:

1
2
3
4
5
6
7
8
main thread start.
thread 1 wait to get locker 1
thread 1 already got locker 1
thread 2 wait to get locker 2
thread 2 already got locker 2
thread 1 wait to get locker 2
thread 2 wait to get locker 1
(程序卡住,永远不会打印后续的 "already got" 消息)

解决线程死锁的案例

解决线程死锁的方案

在 C++ 中,解决线程死锁的方案有以下几种:

  • (1) 破坏互斥条件

    • 使用读写锁(std::shared_mutex)允许多个读
    • 使用无锁编程(std::atomic
  • (2) 破坏持有并等待条件

    • std::lock() 一次性获取所有锁
    • 预先分配所有资源后再执行
  • (3) 破坏不可抢占条件

    • 使用 std::timed_mutex + try_lock_for() 设置超时
    • 超时后释放已持有锁,重试
  • (4) 破坏循环等待条件

    • 所有线程以相同顺序获取锁(最常用)
    • 为锁分配层级,按层级顺序锁定
  • (5) 辅助手段

    • 最小化临界区代码
    • 减少锁的数量,合并临界区
    • 使用 std::lock_guard / std::unique_lock 自动释放锁
    • 使用死锁检测工具(静态分析)
避免死锁的最常见方法破坏的条件复杂度性能适用场景
统一获取锁的顺序循环等待锁数量少,顺序明确
一次性获取所有锁(std::lock()循环等待多锁场景,推荐优先使用
加入超时等待机制(std::timed_mutex + try_lock_for()持有并等待实时系统,要求响应性

使用 std::lock () 一次性获取多个锁

在 C++ 中,std::lock() 能够一次性锁住两个或两个以上的互斥量(std::mutex),至少两个,一个不行。它的核心优势是从根本上避免了因锁的顺序不一致而导致的死锁风险。工作原理是:如果其中某个互斥量没锁住,它就会一直等待,直到所有互斥量都锁住才会继续执行;关键是它保证了要么全部锁住,要么全部没锁住;如果只锁住了其中一个互斥量,而另一个互斥量没锁成功,它会立即把已经锁住的互斥量释放掉。

线程死锁解决案例一
  • 线程死锁解决方案:统一获取锁的顺序
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
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>

class MyClass {
public:
void func1() {
std::cout << "thread 1 wait to get locker 1" << std::endl;

// 等待获取锁 1
lock1.lock();

std::cout << "thread 1 already got locker 1" << std::endl;

// 等待一段时间再获锁 2
std::this_thread::sleep_for(std::chrono::milliseconds(1000));

std::cout << "thread 1 wait to get locker 2" << std::endl;

// 等待获取锁 2
lock2.lock();

std::cout << "thread 1 already got locker 2" << std::endl;

// 释放锁 2
lock2.unlock();

std::cout << "thread 1 already released locker 2" << std::endl;

// 释放锁 1
lock1.unlock();

std::cout << "thread 1 already released locker 1" << std::endl;
}

void func2() {
std::cout << "thread 2 wait to get locker 1" << std::endl;

// 等待获取锁 1
lock1.lock();

std::cout << "thread 2 already got locker 1" << std::endl;

// 等待一段时间再获锁 2
std::this_thread::sleep_for(std::chrono::milliseconds(1000));

std::cout << "thread 2 wait to get locker 2" << std::endl;

// 等待获取锁 2
lock2.lock();

std::cout << "thread 2 already got locker 2" << std::endl;

// 释放锁 2
lock2.unlock();

std::cout << "thread 2 already released locker 2" << std::endl;

// 释放锁 1
lock1.unlock();

std::cout << "thread 2 already released locker 1" << std::endl;
}

private:
std::mutex lock1; // 锁 1
std::mutex lock2; // 锁 2
};

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

MyClass mc;

// 创建并启动线程 1
std::thread t1(&MyClass::func1, &mc);

// 创建并启动线程 2
std::thread t2(&MyClass::func2, &mc);

// 等待线程 1 执行结束
t1.join();

// 等待线程 2 执行结束
t2.join();

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

程序运行的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
main thread start.
thread 1 wait to get locker 1
thread 1 already got locker 1
thread 2 wait to get locker 1
thread 1 wait to get locker 2
thread 1 already got locker 2
thread 1 already released locker 2
thread 1 already released locker 1
thread 2 already got locker 1
thread 2 wait to get locker 2
thread 2 already got locker 2
thread 2 already released locker 2
thread 2 already released locker 1
main thread end.
线程死锁解决案例二

特别注意

使用 std::lock() 锁住多个互斥量(std::mutex)时,如果不使用 std::lock_guardstd::unique_lock,则必须手动调用 std::mutexunlock() 释放锁。强烈建议采用 RAII 方式管理锁,此时需要配合 std::adopt_lock 参数,这个参数的作用是告诉 std::lock_guardstd::unique_lock:互斥量已经被 std::lock() 锁住了,不要在尝试构造时加锁,只需在析构时自动解锁即可。这样做不仅能在作用域结束时自动释放锁,避免因忘记解锁而引发的资源泄漏,还能在临界区抛出异常时保证锁被正确释放,从而大幅提升代码的安全性和健壮性,同时减少手动管理带来的出错风险。

  • 线程死锁解决方案:使用 std::lock() 一次性获取所有锁
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
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>

class MyClass {
public:
void func1() {
std::cout << "thread 1 wait to get locker 1 and locker 2" << std::endl;

// 一次性锁住两个互斥量
std::lock(lock1, lock2);

std::cout << "thread 1 already got locker 1 and locker 2" << std::endl;

// 使用 std::adopt_lock 表明锁已经持有
std::lock_guard<std::mutex> g1(lock1, std::adopt_lock);
std::lock_guard<std::mutex> g2(lock2, std::adopt_lock);

// 模拟业务执行耗时(临界区代码)
std::this_thread::sleep_for(std::chrono::milliseconds(1000));

std::cout << "thread 1 already released locker 1 and locker 2" << std::endl;

// std::lock_guard 析构时会自动释放锁
}

void func2() {
std::cout << "thread 2 wait to get locker 1 and locker 2" << std::endl;

// 一次性锁住两个互斥量(顺序与 func1 不同也没关系)
std::lock(lock1, lock2);

std::cout << "thread 2 already got locker 1 and locker 2" << std::endl;

// 使用 std::adopt_lock 表明锁已经持有
std::lock_guard<std::mutex> g1(lock1, std::adopt_lock);
std::lock_guard<std::mutex> g2(lock2, std::adopt_lock);

// 模拟业务执行耗时(临界区代码)
std::this_thread::sleep_for(std::chrono::milliseconds(1000));

std::cout << "thread 2 already released locker 1 and locker 2" << std::endl;

// std::lock_guard 析构时会自动释放锁
}

private:
std::mutex lock1; // 锁 1
std::mutex lock2; // 锁 2
};

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

MyClass mc;

// 创建并启动线程 1
std::thread t1(&MyClass::func1, &mc);

// 创建并启动线程 2
std::thread t2(&MyClass::func2, &mc);

// 等待线程 1 执行结束
t1.join();

// 等待线程 2 执行结束
t2.join();

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

程序运行的结果如下:

1
2
3
4
5
6
7
8
main thread start.
thread 1 wait to get locker 1 and locker 2
thread 1 already got locker 1 and locker 2
thread 2 wait to get locker 1 and locker 2
thread 1 already released locker 1 and locker 2
thread 2 already got locker 1 and locker 2
thread 2 already released locker 1 and locker 2
main thread end.

unique_lock

unique_lock 是一个通用互斥锁(std::mutex)包装器,通过 RAII(资源获取即初始化)机制自动管理锁的获取与释放,从根本上避免因异常或返回导致的死锁。与简单的 lock_guard 相比,它提供了延迟加锁、尝试加锁、提前解锁和所有权转移等灵活特性,但这也带来稍大的内存与性能开销。其最关键的专属用途是必须配合 condition_variable 使用,因为 wait() 操作需要动态释放和重新获取锁。简言之,unique_lock 是复杂锁定场景与线程同步通信的核心工具,而在只需基础互斥保护的简单场景下,优先考虑轻量的 lock_guardscoped_lock(C++ 17 开始支持)。

unique_lock 的核心作用

  • 核心作用:RAII(资源获取即初始化)锁管理

    • unique_lock 的核心思想是利用对象的构造函数和析构函数,自动管理互斥量的加锁与解锁。
      • 构造时:调用 mutex.lock()(或尝试加锁)。
      • 析构时:自动调用 mutex.unlock()
    • 这带来了最直接的好处:即使代码在执行过程中抛出异常或提前 returnunique_lock 的析构函数也一定会被执行,从而彻底避免死锁(因为锁一定会被释放)。
  • 相较于 lock_guard 的增强(独特优势)

    • 虽然 lock_guard 也能做到 RAII,但 unique_lock 更加灵活。它弥补了 std::mutex 原生接口和 lock_guard 的不足,提供了以下关键特性:
    • 延迟加锁(Deferred Locking):构造时可以不立即加锁,而是稍后在需要时手动调用 lock()。这在需要将锁的创建与加锁分离时非常有用。
    • 尝试加锁(Try Locking):支持 try_lock(), try_lock_for(), try_lock_until(),允许线程在尝试获取锁失败时去做其他事情,而不是阻塞等待,提高了程序的响应性。
    • 所有权转移(Move Semantics):unique_lock 是可移动的(但不可拷贝)。你可以将锁的所有权从一个对象转移给另一个对象,或者从函数中返回一个已加锁的 unique_lock,这极大地方便了在函数间传递锁的管控权。
    • 提前解锁(Manual Unlock):在锁的作用域结束前,可以手动调用 unlock()。这在需要保护一段临界区极短的代码,且后续还有耗时操作时非常有用 —— 提前释放锁以减少锁持有的时间,提高并发度。

  • 与条件变量(condition_variable)配合的强制性要求

    • 这是 unique_lock 最重要的专属职责。C++ 标准规定,condition_variablewait() 系列函数只能接受 unique_lock,不能接受 lock_guard
    • 原因在于 wait() 内部需要原子的释放锁并进入阻塞状态,并且在被唤醒后需要重新获取锁。只有 unique_lock 支持这种动态的加锁 / 解锁 / 所有权转移操作,而 lock_guard 过于死板,无法满足这一需求。
  • unique_lock 的性能与开销权衡

    • 需要注意的是,灵活性是有代价的。unique_lock 内部通常需要维护一个标志位来记录当前是否持有锁(以便在析构时决定是否解锁),因此它的内存占用比 lock_guard 稍大,性能开销也稍高。
    • 如果只需要简单的 RAII 加锁 / 解锁,且不需要上述灵活性:建议使用 lock_guard(或 C++17 的 scoped_lock),它们更轻量且意图更明确。
    • 如果需要延迟加锁、尝试加锁、移动所有权或配合条件变量:请使用 unique_lock

unique_lock 的构造参数

在 C++ 中,unique_lock 的构造函数最常用的参数主要有以下几种:

  • std::adopt_lock_t(接管锁)

    • 假设当前线程已经成功获取了互斥量的锁(比如使用 std::lock()),unique_lock 在构造时不再尝试加锁,而是直接接管该锁的所有权,并在析构时自动解锁。
    • 示例:std::unique_lock<std::mutex> lk(mtx, std::adopt_lock);
  • std::defer_lock_t(延迟加锁)

    • unique_lock 在构造时不加锁,仅持有互斥量的引用。后续需要时再通过调用 lk.lock() 手动加锁。常用于将对象创建与加锁时机分离。
    • 示例:std::unique_lock<std::mutex> lk(mtx, std::defer_lock);
  • std::try_to_lock_t(尝试加锁)

    • unique_lock 在构造时调用 mutex.try_lock() 尝试加锁。加锁结果可通过 lk.owns_lock() 判断,即使加锁失败也不会阻塞线程,允许程序做其他处理。
    • 示例:std::unique_lock<std::mutex> lk(mtx, std::try_to_lock);
  • 超时时间(时间点或等待时长)

    • unique_lock 在构造时尝试加锁,但若无法立即获取,则阻塞等待直至超时。支持相对时长(std::chrono::duration)或绝对时间点(std::chrono::time_point)。
    • 示例:
      • std::unique_lock<std::timed_mutex> lk(mtx, std::chrono::seconds(1));(等待 1 秒)
      • std::unique_lock<std::timed_mutex> lk(mtx, std::chrono::steady_clock::now() + std::chrono::seconds(1));
  • 默认情况(无参数):

    • unique_lock 在构造时立即调用 mutex.lock(),阻塞当前线程直到获取锁为止。

特别注意

使用 unique_lock 时需注意,除默认构造函数外,以上参数必须与互斥量类型(std::mutex)匹配(例如,超时参数要求互斥量支持 try_lock_fortry_lock_until,即 std::timed_mutex 等)。

unique_lock 的成员函数

unique_lock 的常用成员函数主要分为锁操作、状态查询和所有权管理三类:

  • 锁操作(加锁 / 解锁)

    • lock():手动加锁。若互斥量已被其他线程持有,则阻塞等待,需在未持有锁时调用。
    • try_lock():尝试加锁。立即返回,成功返回 true,失败返回 false,不阻塞线程。
    • try_lock_for(duration):尝试加锁,等待指定的相对时长(需互斥量支持 try_lock_for(),如 std::timed_mutex)。
    • try_lock_until(time_point):尝试加锁,等待直到指定的绝对时间点(需互斥量支持 try_lock_until)。
    • unlock():手动解锁。通常用于提前释放锁,减少临界区持有时间,需确保当前线程持有锁。
  • 锁状态查询

    • owns_lock():返回 bool,表示当前是否持有锁。常用于 try_lock() 系列操作后判断是否成功获取锁。
    • operator bool():与 owns_lock() 功能相同,允许在条件判断中直接使用对象(如 if (lk) { ... })。
    • mutex():返回指向所管理互斥量的指针(不常用,一般用于代码调试)。
  • 所有权管理

    • release():释放所有权,即解除 unique_lock 与互斥量(std::mutex)的关联,但不解锁互斥量。返回互斥量指针,调用者需负责后续手动解锁。常用于转移锁管理责任。
    • 移动语义(非成员函数风格):unique_lock 可移动(包括移动构造函数和移动赋值运算符),允许将锁的所有权从一个对象转移给另一个对象。

  • 成员函数的使用案例
1
2
3
4
5
6
7
8
9
10
std::timed_mutex mtx;
std::unique_lock<std::timed_mutex> lk(mtx, std::defer_lock); // 延迟加锁

// 尝试获取锁,最多等待 100 毫秒
if (lk.try_lock_for(std::chrono::milliseconds(100))) {
// 成功获取锁,操作共享数据
lk.unlock(); // 提前释放锁
} else {
// 超时等待未获得锁,做其他处理
}

特别注意

  • unique_locklock()unlock() 必须成对使用,且调用前需确认当前锁状态,否则可能导致未定义行为(如重复加锁或解锁未持有的锁)。
  • unique_lock 调用 release() 后,unique_lock 析构时不会再自动解锁,调用者需负责后续手动解锁,使用时需格外谨慎。
  • unique_lock 在大部分场景下,开发者无需手动调用 unlock(),利用 RAII 自动析构即可;手动解锁主要用于优化性能(提前释放锁)。

unique_lock 的所有权传递

在 C++ 中,unique_lock 遵循移动语义,其互斥量(std::mutex)的所有权可以转移,但不能复制。这意味着一个 unique_lock 对象可以通过移动构造函数或移动赋值运算符将其对互斥量的管理权交给另一个 unique_lock 对象,转移后原 unique_lock 对象不再持有锁,从而避免多个 unique_lock 对象同时管理同一互斥量。这种设计既保证了资源管理的唯一性,又提供了在不同作用域或函数间传递锁所有权的灵活性。

特别注意

  • unique_lock 对象不能被复制:如果试图写 std::unique_lock<std::mutex> lock2 = lock;(拷贝),编译器会报错。
  • unique_lock 所有权转移后,原 unique_lock 对象会变为空状态(不持有任何锁),其 owns_lock() 返回 false,析构时不会尝试解锁。
  • unique_lock 所有权转移后,不能再对原 unique_lock 对象调用 lock()unlock() 等方法,否则会导致未定义行为(程序崩溃、运行时抛出异常等)。
  • 案例代码一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <mutex>

int main() {
std::mutex mx;
std::unique_lock<std::mutex> lock1(mx);

// 非法代码(编译出错),所有权是不允许复制的
// std::unique_lock<std::mutex> lock2(lock1);
// std::unique_lock<std::mutex> lock2 = lock1;

// 合法代码(编译通过),所有权是允许转移的
// 所有权转移后,lock1 的互斥量指向为空(不持有任何锁),而 lock2 的互斥量指向为 mx
std::unique_lock<std::mutex> lock2(std::move(lock1));

return 0;
}
  • 案例代码二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <mutex>

std::unique_lock<std::mutex> getLock(std::mutex &mx) {
std::unique_lock<std::mutex> lock(mx);
// 函数结果返回 unique_lock 局部对象是可以的,系统会生成临时的 unique_lock 对象,并调用 unique_lock 的移动构造函数
return lock;
}

int main() {
std::mutex mx;
std::unique_lock<std::mutex> lock = getLock(mx);
return 0;
}

unique_lock 的完整使用案例

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::unique_lock<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::unique_lock<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

单例设计模式

在 C++ 中使用单例模式时,一般不用管单例对象的内存释放问题,也就是不需要编写自定义的析构函数。

单例设计模式实现

饿汉单例模式

饿汉单例模式是指还没有获取单例对象,单例对象就已经创建完成(初始化)了。值得一提的是,饿汉单例模式是线程安全的,因为单例对象在 main() 函数执行前就已经初始化完成(静态初始化),所以是线程安全的(因为对象已经存在,只是返回指针)。

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

using namespace std;

class MyClass {
private:
// 私有构造函数
MyClass() {
cout << "MyClass()" << endl;
}

// 私有析构函数
~MyClass() {
cout << "~MyClass()" << endl;
}

// 删除拷贝构造函数
MyClass(const MyClass&) = delete;

// 删除赋值操作运算符
MyClass& operator=(const MyClass&) = delete;

public:
// 获取单例对象(静态方法)
static MyClass* getInstance() {
return m_instance;
}

private:
// 静态成员变量
static MyClass* m_instance;
};

// 类外初始化静态变量(单例对象),分配内存空间
MyClass* MyClass::m_instance = new MyClass();

int main() {
// 获取单例对象
MyClass* mc = MyClass::getInstance();
MyClass* mc2 = MyClass::getInstance();

// 判断地址是否相同
cout << "address: " << mc << endl;
cout << "address: " << mc2 << endl;
cout << "equals: " << (mc == mc2 ? "true" : "false") << endl;

return 0;
}

程序运行的结果如下:

1
2
3
4
MyClass()
address: 0x55a0fe5b4eb0
address: 0x55a0fe5b4eb0
equals: true

资源泄漏问题

  • 上面饿汉单例模式的代码存在资源泄漏问题,不推荐使用(建议使用静态局部变量实现单例模式),原因如下:
  • (1) 对象在堆上分配内存,但从未被 delete,内存不会被释放;
  • (2) 由于析构函数是 private,即使开发者想手动 delete 也做不到;
  • (3) 当程序退出时,对象的析构函数不会被调用,资源永远无法得到释放。
懒汉单例模式

懒汉单例模式是指在第一次被使用时才创建单例对象,而不是程序启动时就创建。特别注意,懒汉单例模式不一定是线程安全的,由具体的代码实现决定。线程安全的懒汉单例模式有以下几种实现方式:

实现方式是否线程安全防止指令重排推荐指数
静态局部变量✅(需要 C++ 11 及以上的编译器)⭐⭐⭐⭐(最推荐,最简洁)
mutex + atomic + DCL✅(兼容 C++ 11 以下的编译器,如 C++ 98 / C++ 03⭐⭐⭐(适合对底层控制有要求的场景)

C++ 基于静态局部变量实现懒汉单例模式

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

using namespace std;

class MyClass {
private:
// 私有构造函数
MyClass() {
cout << "MyClass()" << endl;
}

// 私有析构函数
~MyClass() {
cout << "~MyClass()" << endl;
}

// 删除拷贝构造函数
MyClass(const MyClass&) = delete;

// 删除赋值操作运算符
MyClass& operator=(const MyClass&) = delete;

public:
// 获取单例对象(静态方法)
static MyClass* getInstance() {
// 静态局部变量(线程安全)
static MyClass instance;
return &instance;
}
};

int main() {
// 获取单例对象
MyClass* mc = MyClass::getInstance();
MyClass* mc2 = MyClass::getInstance();

// 判断地址是否相同
cout << "address: " << mc << endl;
cout << "address: " << mc2 << endl;
cout << "equals: " << (mc == mc2 ? "true" : "false") << endl;

return 0;
}

程序运行的结果如下:

1
2
3
4
5
MyClass()
address: 0x55b578666192
address: 0x55b578666192
equals: true
~MyClass()

C++ 基于 DCL(双重检查锁)实现懒汉单例模式

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

class MyClass {
private:
// 私有构造函数
MyClass() {
std::cout << "MyClass()" << std::endl;
}

// 私有析构函数
~MyClass() {
std::cout << "~MyClass()" << std::endl;
}

// 删除拷贝构造函数
MyClass(const MyClass&) = delete;

// 删除赋值操作运算符
MyClass& operator=(const MyClass&) = delete;

public:
// 获取单例对象(静态方法)
static MyClass* getInstance() {
// 第一次获取单例对象
MyClass* ptr = m_instance.load(std::memory_order_acquire);

// 第一次检测单例对象是否为空
if (ptr == nullptr) {
// 获取互斥锁
std::lock_guard<std::mutex> lock(m_mutex);

// 第二次获取单例对象
ptr = m_instance.load(std::memory_order_relaxed);

// 第二次检测单例对象是否为空
if (ptr == nullptr) {
// 初始化单例对象
ptr = new MyClass();

// 设置单例对象
m_instance.store(ptr, std::memory_order_release);
}
}

// 返回单例对象
return ptr;
}

// 销毁单例对象(静态方法)
static void destroyInstance() {
// 获取互斥锁
std::lock_guard<std::mutex> lock(m_mutex);

// 获取单例对象
MyClass* ptr = m_instance.exchange(nullptr);

// 释放单例对象
if (ptr != nullptr) {
delete ptr;
}
}

private:
static std::mutex m_mutex; // 互斥锁(静态变量)
static std::atomic<MyClass*> m_instance; // 单例对象(静态变量)
};

// 类外初始化静态变量(互斥锁)
std::mutex MyClass::m_mutex;

// 类外初始化静态变量(单例对象)
std::atomic<MyClass*> MyClass::m_instance(nullptr);

int main() {
// 获取单例对象
MyClass* mc = MyClass::getInstance();
MyClass* mc2 = MyClass::getInstance();

// 判断地址是否相同
std::cout << "address: " << mc << std::endl;
std::cout << "address: " << mc2 << std::endl;
std::cout << "equals: " << (mc == mc2 ? "true" : "false") << std::endl;

// 释放单例对象
MyClass::destroyInstance();

return 0;
}

程序运行的结果如下:

1
2
3
4
5
MyClass()
address: 0x55ec9d5bbeb0
address: 0x55ec9d5bbeb0
equals: true
~MyClass()

在写单例模式时,很多开发者喜欢提供一个类似 destroyInstance() 的方法来手动释放内存。开发者可能觉得自己在里面加了互斥锁(Mutex)和原子操作(Atomic)就已经万无一失了,但实际上,这在多线程环境下埋下了一个致命的隐患。

  • 假设有以下并发场景:

    • (1) 线程 A 调用 getInstance(),通过了空指针检查,成功拿到了单例对象的指针 ptr
    • (2) 线程 B 此时突然调用了 destroyInstance()。因为有锁保护,它成功将内部指针置空,并无情地执行了 delete ptr;
    • (3) 线程 A 根本不知道单例对象已经被销毁。它兴高采烈地拿着手里的 ptr 去调用成员函数(例如 ptr->doSomething())。
  • 错误原因分析:

    • 错误结果:线程 A 访问了已经被释放内存的单例对象指针(野指针 / 悬空指针),引发未定义行为,导致程序直接崩溃。
    • 核心矛盾:锁和原子操作只能保护单例 “指针本身” 的修改是安全的,但它们无法保护其他线程手中已经持有的、指向单例内存的存根。
    • 最佳实践:在现代 C++(C++11 及以后)中,永远不要尝试手动销毁单例对象。最优雅、最安全的做法是使用静态局部变量来实现单例模式。
  • 正常释放单例对象的内存:

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

class MyClass {
private:
// 私有构造函数
MyClass() {
std::cout << "MyClass()" << std::endl;
}

// 私有析构函数
~MyClass() {
std::cout << "~MyClass()" << std::endl;
}

// 删除拷贝构造函数
MyClass(const MyClass&) = delete;

// 删除赋值操作运算符
MyClass& operator=(const MyClass&) = delete;

public:
// 获取单例对象(静态方法)
static MyClass* getInstance() {
// 第一次获取单例对象
MyClass* ptr = m_instance.load(std::memory_order_acquire);

// 第一次检测单例对象是否为空
if (ptr == nullptr) {
// 获取互斥锁
std::lock_guard<std::mutex> lock(m_mutex);

// 第二次获取单例对象
ptr = m_instance.load(std::memory_order_relaxed);

// 第二次检测单例对象是否为空
if (ptr == nullptr) {
// 初始化单例对象
ptr = new MyClass();

// 静态局部变量(用于自动释放单例对象的内存)
static GcClass gc;

// 设置单例对象
m_instance.store(ptr, std::memory_order_release);
}
}

// 返回单例对象
return ptr;
}

// 内部定义的 GC 类,用于自动释放单例对象的内存
class GcClass {
public:
// 构造函数
GcClass() {
}

// 析构函数
~GcClass() {
MyClass* ptr = MyClass::getInstance();
if (ptr == nullptr) {
// 释放单例对象的内存
delete ptr;
ptr = nullptr;
}
}
};

private:
static std::mutex m_mutex; // 互斥锁(静态变量)
static std::atomic<MyClass*> m_instance; // 单例对象(静态变量)
};

// 类外初始化静态变量(互斥锁)
std::mutex MyClass::m_mutex;

// 类外初始化静态变量(单例对象)
std::atomic<MyClass*> MyClass::m_instance(nullptr);

int main() {
// 获取单例对象
MyClass* mc = MyClass::getInstance();
MyClass* mc2 = MyClass::getInstance();

// 判断地址是否相同
std::cout << "address: " << mc << std::endl;
std::cout << "address: " << mc2 << std::endl;
std::cout << "equals: " << (mc == mc2 ? "true" : "false") << std::endl;

return 0;
}

程序运行的结果如下:

1
2
3
4
5
MyClass()
address: 0x55c7c52d6eb0
address: 0x55c7c52d6eb0
equals: true
~MyClass()

共享数据问题分析

在多线程环境中,下面的单例设计模式代码存在严重的线程安全问题(共享数据问题)。这是因为在多线程环境下,下述代码的线程安全问题核心在于 getInstance() 中的 “检查 - 然后 - 操作” 竞态条件:当多个线程同时首次调用 getInstance() 并检测到 m_instance == nullptr 时,它们都会通过 new MyClass() 创建各自独立的实例,导致单例规则被破坏,并且由于析构函数私有,这些多余的对象永远无法被正确释放,造成内存泄漏;同时,new 操作本身涉及内存分配和构造两个步骤,若无内存屏障保护,其他线程可能看到一个未完全构造完成的对象,进而引发未定义行为。

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

class MyClass {
private:
// 私有构造函数
MyClass() {
std::cout << "MyClass()" << std::endl;
}

// 私有析构函数
~MyClass() {
std::cout << "~MyClass()" << std::endl;
}

// 删除拷贝构造函数
MyClass(const MyClass&) = delete;

// 删除赋值操作运算符
MyClass& operator=(const MyClass&) = delete;

public:
// 获取单例对象(静态方法)
static MyClass* getInstance() {
if (m_instance == nullptr) {
// 初始化单例对象
m_instance = new MyClass();
}

// 返回单例对象
return m_instance;
}

private:
static MyClass* m_instance; // 单例对象(静态变量)
};

// 类外初始化静态变量(单例对象)
MyClass* MyClass::m_instance(nullptr);

// 线程函数
void run() {
MyClass* ptr = MyClass::getInstance();
std::cout << "address: " << ptr << std::endl;
}

int main() {
std::thread t1(run);
std::thread t2(run);
std::thread t3(run);

// 等待子线程执行完成
t1.join();
t2.join();
t3.join();

return 0;
}

程序运行的结果如下(可以发现 MyClass 类的构造函数被调用了多次,破坏了单例规则):

1
2
3
4
5
6
MyClass()
address: 0x7f84e4000b70
MyClass()
address: 0x7f84dc000b70
MyClass()
address: 0x7f84ec000b70

共享数据问题解决

在多线程环境中,可以使用互斥量(mutex)+ 原子操作(atomic) + DCL(双重检查锁)来解决上面单例设计模式代码存在的线程安全问题(共享数据问题)。

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

class MyClass {
private:
// 私有构造函数
MyClass() {
std::cout << "MyClass()" << std::endl;
}

// 私有析构函数
~MyClass() {
std::cout << "~MyClass()" << std::endl;
}

// 删除拷贝构造函数
MyClass(const MyClass&) = delete;

// 删除赋值操作运算符
MyClass& operator=(const MyClass&) = delete;

public:
// 获取单例对象(静态方法)
static MyClass* getInstance() {
// 第一次获取单例对象
MyClass* ptr = m_instance.load(std::memory_order_acquire);

// 第一次检测单例对象是否为空
if (ptr == nullptr) {
// 获取互斥锁
std::lock_guard<std::mutex> lock(m_mutex);

// 第二次获取单例对象
ptr = m_instance.load(std::memory_order_relaxed);

// 第二次检测单例对象是否为空
if (ptr == nullptr) {
// 初始化单例对象
ptr = new MyClass();

// 设置单例对象
m_instance.store(ptr, std::memory_order_release);
}
}

// 返回单例对象
return ptr;
}

private:
static std::mutex m_mutex; // 互斥锁(静态变量)
static std::atomic<MyClass*> m_instance; // 单例对象(静态变量)
};

// 类外初始化静态变量(互斥锁)
std::mutex MyClass::m_mutex;

// 类外初始化静态变量(单例对象)
std::atomic<MyClass*> MyClass::m_instance(nullptr);

// 线程函数
void run() {
MyClass* ptr = MyClass::getInstance();
std::cout << "address: " << ptr << std::endl;
}

int main() {
std::thread t1(run);
std::thread t2(run);
std::thread t3(run);

// 等待子线程执行完成
t1.join();
t2.join();
t3.join();

return 0;
}

程序运行的结果如下(可以发现 MyClass 类的构造函数不会被调用多次,单例规则没有被破坏):

1
2
3
4
MyClass()
address: 0x7f44e0000b70
address: 0x7f44e0000b70
address: 0x7f44e0000b70