C++ 多线程编程之一

C++ 核心概念

互斥锁的介绍

  • mutex
    • 使用语法(mtx.lock();mtx.unlock();)。
    • 最基础的互斥量类型(原始锁),用于保护共享资源,防止多个线程并发访问共享资源。
    • 如果在某个程序分支或异常路径中忘了执行 mtx.unlock(),就容易导致线程死锁。
    • mutex 不支持同一个线程重复对其加锁,即无法用于实现递归锁(可重入锁)。
    • 如果希望实现递归锁(可重入锁),可以使用 recursive_mutex 来替代。recursive_mutex 支持同一个线程对 mutex 重复加锁,每次加锁都需要对应的解锁。
  • lock_guard

    • 使用语法(lock_guard<mutex> lock(_mutex);)。
    • 一种轻量级的锁管理类,它在构造时自动加锁,在析构时自动释放锁(RAII 技术),适合在指定作用域内自动管理锁。
    • 作用域结束时自动解锁,不可以手动解锁(无法提前释放锁)。
    • lock_guardrecursive_mutex 搭配使用,可以实现递归锁(可重入锁)。
    • unique_lock 更轻量级,性能更好(因为没有 unlock () 之类的额外操作)。
    • lock_guard 不能作为函数参数的类型或者函数返回值的类型,只能用在简单的临界区代码段的互斥操作中。
  • unique_lock

    • 使用语法(unique_lock<mutex> lock(_mutex);)。
    • 一种更灵活的锁管理类,它在构造时自动加锁,在析构时自动释放锁(RAII 技术)。
    • 可以手动解锁(lock.unlock())。
    • 可以延迟加锁(unique_lock<mutex> lock(_mutex, defer_lock);)。
    • 可以移动赋值(unique_lock 可被转移,但 lock_guard 不能被转移)。
    • unique_lockrecursive_mutex 搭配使用,可以实现递归锁(可重入锁)。
    • 通常与 condition_variable 一起配合使用,因为 condition_variable::wait() 函数需要传入 unique_lock 参数(不能使用 lock_guard)。

对比表格

特性mutexlock_guardunique_lock
是否自动管理锁❌ 否✅ 是(RAII)✅ 是(RAII)
是否可解锁再加锁✅ 是❌ 否✅ 是
是否可延迟加锁❌ 否❌ 否✅ 是
支持条件变量(CV)❌ 否❌ 否✅ 是(推荐配合使用)
资源占用最低略高(但更灵活)

总结

  • 如果需要手动控制解锁,建议使用 unique_lock
  • 如果只需要简单加锁 / 解锁,建议使用 lock_guard,效率更高。

unique_lock 的作用

unique_lock 是 C++ 标准库 mutex 的 RAII(资源获取即初始化)封装,用于自动管理互斥锁。当执行 unique_lock<mutex> lock(_mutex); 时:

  • 当前线程尝试获取 mutex 锁(如果其他线程已经持有锁,则当前线程会阻塞等待,直到锁被释放)。
  • 一旦成功获取锁,在 lock 对象的生命周期内,当前线程独占访问受保护的资源。
  • lock 对象销毁时(如作用域结束),mutex 会自动解锁,从而可以避免死锁或资源泄露。

condition_variable 的作用

  • 核心概念

    • condition_variable 是 C++ 11 提供的一种线程间同步工具,用于线程等待某个条件满足,并在条件满足时被其他线程唤醒。
    • condition_variable 常与 mutexunique_lock 一起搭配使用,可用于实现线程间等待通知机制,类似 Java 中的 wait / notify
  • 注意事项

    • 等待时必须传入一把锁(如 mutex),否则 wait() 会报错。
    • 执行 wait() 后,线程会挂起等待,同时释放锁,被唤醒后重新获取锁。
    • 推荐使用带 Predicate(断言)wait(),防止虚假唤醒。
    • notify_one() 要在修改共享状态之后再调用,否则会错过通知。
    • 常配合 unique_lock 一起使用,因为 unique_locklock_guard 更灵活(支持手动解锁、延迟加锁等)。
  • 适用场景

    • 生产者 / 消费者模型
    • 线程间通知与协作
    • 线程等待某种条件成立(如队列不为空)
  • 基本操作

函数含义
wait(lock)线程挂起等待,同时释放锁,被唤醒后重新获取锁。
wait(lock, predicate)等待直到条件成立,否则继续阻塞,可以防止虚假唤醒。等待期间释放锁,被唤醒后重新获取锁。
notify_one()唤醒一个等待中的线程。
notify_all()唤醒所有等待中的线程。

什么是虚假唤醒

  • 虚假唤醒是指线程在没有收到 Notify(通知)或没有满足条件的情况下意外醒来。
  • 换言之,线程在 wait() 处本应该阻塞,但它自己突然 "醒了",而并没有任何线程调用 notify_one()notify_all() 触发它醒来,也没有任何共享条件真正发生变化。
  • 虚假唤醒不是 Bug,它是操作系统 / CPU 实现层面允许的行为。主要原因有:(1) 出于性能或调度策略考虑,某些系统可能让线程偶尔 "意外醒来";(2) 多线程调度可能出现不可预知的唤醒;(3) 这在 POSIX 线程规范(pthread)和 C++ 标准中都是被允许的。

C++ 多线程编程

这里介绍的是 C++ 语言级别的多线程编程,支持跨平台编译与运行。值得一提的是,现代 C++ 语言层面的多线程编程使用的是 thread,而其底层的实现依旧是区分不同平台的,比如:Windows 平台使用的是 createThread,Linux 平台使用的是 pthread_create

C++ 多线程编程的核心技术

  • thread / mutex / condition_variable
  • unique_lock / lock_guard
  • atomic(基于 CAS 的原则类型)
  • sleep_for

thread 的使用

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

void threadHandler1() {
// 线程睡眠5秒
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "run thread handler 1" << std::endl;
}

void threadHandler2(int seconds) {
// 线程睡眠
std::this_thread::sleep_for(std::chrono::seconds(seconds));
std::cout << "run thread handler 2" << std::endl;
}

int main() {

// 定义一个线程对象,参数传入一个线程函数,线程会自动启动
std::thread t1(threadHandler1);

// 或者指定线程函数的参数
// std::thread t1(threadHandler2, 5);

// 主线程等待子线程结束,然后再继续往下执行
t1.join();

// 或者将子线程设置为分离线程,这样主线程就不需要等待子线程结束;当主线程结束,所有子线程都自动结束(终止运行)
// t1.detach();

return 0;
}

程序运行的输出结果如下:

1
run thread handler 1

实现线程间的互斥

这里将使用 std::mutexstd::lock_guard 来模拟车站三个售票窗口同时售票,要求每张票只能卖一次。

提示

  • std::lock_guardstd::unique_lock 都是 C++ 标准库 std::mutex 的 RAII(资源获取即初始化)封装,用于自动管理互斥锁(如自动解锁)。
  • std::lock_guard 支持在作用域结束时自动解锁,但不可以手动解锁,比 std::unique_lock 更轻量级,性能更好。
  • std::unique_lock 支持自动 / 手动解锁、延迟加锁、移动赋值等,通常与 std::condition_variable 搭配使用。
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
#include <iostream>
#include <list>
#include <thread>
#include <mutex>

// 互斥锁
std::mutex _mutex;

// 车站一共有 100 张车票
int ticketCount = 100;

void sellTicket(int id) {
while (ticketCount > 0) {
// 获取互斥锁(支持在作用域结束时自动解锁)
std::lock_guard<std::mutex> lock(_mutex);

// 双重检测
if (ticketCount > 0) {
// 执行业务逻辑
std::cout << "售票窗口 " << id << " 卖出第 " << ticketCount << " 张车票" << std::endl;
ticketCount--;
}

std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}

int main() {
const int threadCount = 3;
std::list<std::thread> threads;

// 启动三个售票子线程
for (int i = 0; i < threadCount; i++) {
threads.push_back(std::thread(sellTicket, i));
}

// 阻塞等待三个售票子线程执行结束
for (std::thread& t : threads) {
t.join();
}

std::cout << "所有售票窗口已经结束售票" << std::endl;

return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
6
7
8
9
10
售票窗口 0 卖出第 100 张车票
售票窗口 0 卖出第 99 张车票
售票窗口 0 卖出第 98 张车票
售票窗口 0 卖出第 97 张车票
......
售票窗口 1 卖出第 4 张车票
售票窗口 1 卖出第 3 张车票
售票窗口 2 卖出第 2 张车票
售票窗口 2 卖出第 1 张车票
所有售票窗口已经结束售票

实现线程间的同步通信

这里实现生产者 / 消费者线程模型,使用了 std::threadstd::mutexstd::unique_lockstd::condition_variable。主要模拟实现生产者线程生产一条数据,消费者线程就立刻消费一条数据,两个线程一直交替执行。

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

using namespace std;

mutex _mutex; // 互斥锁
queue<int> _queue; // 共享数据队列
condition_variable _cv; // 条件变量
atomic_bool _finish(false); // 控制标志(用于终止程序)

void produce() {
while (!_finish) {
// 获取锁
unique_lock<mutex> lock(_mutex);

// 阻塞等待,且等待期间会自动释放锁,当收到通知或满足条件(可以避免虚假唤醒)时,才会重新获得互斥锁并继续执行
_cv.wait(lock, [] { return _queue.empty(); });

// 模拟生产时间
this_thread::sleep_for(chrono::milliseconds(500));

int item = rand() % 1000 + 1;
_queue.push(item);
cout << "生产: " << item << endl;

// 通知消费者
_cv.notify_all();
}
}

void consume() {
while (!_finish) {
// 获取锁
unique_lock<mutex> lock(_mutex);

// 阻塞等待,且等待期间会自动释放锁,当收到通知或满足条件(可以避免虚假唤醒)时,才会重新获得互斥锁并继续执行
_cv.wait(lock, [] { return !_queue.empty(); });

// 模拟消费时间
this_thread::sleep_for(chrono::milliseconds(500));

int item = _queue.front();
_queue.pop();
cout << "消费: " << item << endl;

// 通知生产者
_cv.notify_all();
}
}

int main() {
// 设置随机种子
srand(time(nullptr));

// 启动生产者和消费者线程
thread producer(produce);
thread consumer(consume);

// 主线程运行 60 秒
this_thread::sleep_for(chrono::seconds(60));

// 设置终止标志
_finish = true;

// 唤醒所有线程,确保它们能结束
_cv.notify_all();

// 主线程等待生产者和消费者线程结束
producer.join();
consumer.join();

return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
生产: 98
消费: 98
生产: 635
消费: 635
生产: 318
消费: 318
生产: 513
消费: 513
生产: 413
消费: 413
......

C++ 原子类型

提示

CAS(比较与交换)是一种轻量级、不需要加锁的线程同步机制,可用于防止多个线程同时读写共享变量时发生数据竞争。值得一提的是,C++ 原子类型的底层都是基于 CAS 实现的。

atomic 原子类型的概念

  • 核心概念

    • std::atomic<T> 是 C++ 11 引入的一个模板类,用于实现线程安全的原子操作。它的本质是:对变量的读写不可被中断,不需要加锁(基于 CAS),但又是线程安全的。
    • 在多线程编程中,如果两个线程同时读写一个普通变量(如 int),就可能产生竞态条件。使用 std::atomic 可以避免这种问题,而不需要显式使用 mutex 互斥锁。
    • std::atomic 本身就已经保证了原子性、内存可见性和有序性(默认情况下),不需要也不应该再搭配 volatile 一起使用。
    • 为了使用方便,C++ 标准库为常用的原始类型提供了特定的类型别名,例如:
      • std::atomic_intstd::atomic<int> 的类型别名。
      • std::atomic_boolstd::atomic<bool> 的类型别名。
      • 其他类似的别名还有 std::atomic_charstd::atomic_long 等。
  • 支持的类型

    • 基本类型:intboolcharfloat 等。
    • 用户自定义的类型,但该类型必须是平凡可复制的(Trivially Copyable)。这意味着该类型的对象可以通过简单的内存复制进行复制,比如通过 memcpy 这样的操作进行复制,而无需调用构造函数或者赋值运算符。
  • 支持的操作

    • 原子读写:load() / store()
    • 自增自减:++--fetch_add()fetch_sub()
    • 比较并交换(CAS):compare_exchange_strong() / compare_exchange_weak()
  • 使用注意事项

    • std::atomic 不支持拷贝赋值(复制是被禁用的)。
    • 默认是顺序一致性(memory_order_seq_cst),内存可见性是有保障的。
    • 支持内存顺序优化(高级用法),包括 memory_order_relaxedacquirerelease 等。

volatile 能干什么?它和 atomic 是一回事吗?

特别注意,volatilestd::atomic 不是一回事!volatile 在 C++ 里的作用和你想的不太一样。

功能std::atomicvolatile
保证原子性?✅ 是❌ 否
保证内存可见性?✅ 是❌ 否
保证编译器不优化读写?✅是(控制更强)✅是(仅编译器层面)
多线程同步安全?✅ 是❌ 否
  • 结论:
    • volatile 只告诉编译器 “不要优化这个变量的访问”。
    • volatile 不会保证线程间同步,不会保证缓存刷新,不会保证乱序执行的控制。
    • 所以,在 C++ 多线程编程里,volatile 基本没用(少数平台 / 驱动除外)。

atomic 原子类型的使用

这里将演示如何使用基于 CAS 操作的 atomic 原子类型,比如 atomic_intatomic_bool

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

std::atomic_int _count(0);
std::atomic_bool _isReady(false);

void taskHandler() {
while (!_isReady) {
// 线程出让当前的 CPU 时间片,等待下一次调度的执行
std::this_thread::yield();
}

for (int i = 0; i < 100; i++) {
_count++;
}
}

int main() {
std::list<std::thread> list;

// 创建多个子线程
for (int i = 0; i < 10; i++) {
list.push_back(std::thread(taskHandler));
}

// 主线程等待几秒
std::this_thread::sleep_for(std::chrono::seconds(5));

_isReady = true;

// 等待所有子线程执行结束
for (auto it = list.begin(); it != list.end(); ++it) {
(*it).join();
}

std::cout << "_count = " << _count << std::endl;

return 0;
}

程序运行的输出结果如下:

1
_count = 1000