C++ 多线程编程之一
C++ 核心概念
互斥锁的介绍
mutex- 使用语法(
mtx.lock();、mtx.unlock();)。 - 最基础的互斥量类型(原始锁),用于保护共享资源,防止多个线程并发访问共享资源。
- 如果在某个程序分支或异常路径中忘了执行
mtx.unlock(),就容易导致线程死锁。 mutex不支持同一个线程重复对其加锁,即无法用于实现递归锁(可重入锁)。- 如果希望实现递归锁(可重入锁),可以使用
recursive_mutex来替代。recursive_mutex支持同一个线程对mutex重复加锁,每次加锁都需要对应的解锁。
- 使用语法(
lock_guard- 使用语法(
lock_guard<mutex> lock(_mutex);)。 - 一种轻量级的锁管理类,它在构造时自动加锁,在析构时自动释放锁(RAII 技术),适合在指定作用域内自动管理锁。
- 作用域结束时自动解锁,不可以手动解锁(无法提前释放锁)。
lock_guard和recursive_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_lock和recursive_mutex搭配使用,可以实现递归锁(可重入锁)。- 通常与
condition_variable一起配合使用,因为condition_variable::wait()函数需要传入unique_lock参数(不能使用lock_guard)。
- 使用语法(
对比表格
| 特性 | mutex | lock_guard | unique_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常与mutex和unique_lock一起搭配使用,可用于实现线程间等待通知机制,类似 Java 中的wait / notify。
注意事项
- 等待时必须传入一把锁(如
mutex),否则wait()会报错。 - 执行
wait()后,线程会挂起等待,同时释放锁,被唤醒后重新获取锁。 - 推荐使用带
Predicate(断言)的wait(),防止虚假唤醒。 notify_one()要在修改共享状态之后再调用,否则会错过通知。- 常配合
unique_lock一起使用,因为unique_lock比lock_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 |
|
程序运行的输出结果如下:
1 | run thread handler 1 |
实现线程间的互斥
这里将使用 std::mutex 与 std::lock_guard 来模拟车站三个售票窗口同时售票,要求每张票只能卖一次。
提示
std::lock_guard与std::unique_lock都是 C++ 标准库std::mutex的 RAII(资源获取即初始化)封装,用于自动管理互斥锁(如自动解锁)。std::lock_guard支持在作用域结束时自动解锁,但不可以手动解锁,比std::unique_lock更轻量级,性能更好。std::unique_lock支持自动 / 手动解锁、延迟加锁、移动赋值等,通常与std::condition_variable搭配使用。
1 |
|
程序运行的输出结果如下:
1 | 售票窗口 0 卖出第 100 张车票 |
实现线程间的同步通信
这里实现生产者 / 消费者线程模型,使用了 std::thread、std::mutex、std::unique_lock、std::condition_variable。主要模拟实现生产者线程生产一条数据,消费者线程就立刻消费一条数据,两个线程一直交替执行。
1 |
|
程序运行的输出结果如下:
1 | 生产: 98 |
C++ 原子类型
提示
CAS(比较与交换)是一种轻量级、不需要加锁的线程同步机制,可用于防止多个线程同时读写共享变量时发生数据竞争。值得一提的是,C++ 原子类型的底层都是基于 CAS 实现的。
atomic 原子类型的概念
核心概念
std::atomic<T>是 C++ 11 引入的一个模板类,用于实现线程安全的原子操作。它的本质是:对变量的读写不可被中断,不需要加锁(基于 CAS),但又是线程安全的。- 在多线程编程中,如果两个线程同时读写一个普通变量(如
int),就可能产生竞态条件。使用std::atomic可以避免这种问题,而不需要显式使用mutex互斥锁。 std::atomic本身就已经保证了原子性、内存可见性和有序性(默认情况下),不需要也不应该再搭配volatile一起使用。- 为了使用方便,C++ 标准库为常用的原始类型提供了特定的类型别名,例如:
std::atomic_int是std::atomic<int>的类型别名。std::atomic_bool是std::atomic<bool>的类型别名。- 其他类似的别名还有
std::atomic_char、std::atomic_long等。
支持的类型
- 基本类型:
int、bool、char、float等。 - 用户自定义的类型,但该类型必须是平凡可复制的(Trivially Copyable)。这意味着该类型的对象可以通过简单的内存复制进行复制,比如通过
memcpy这样的操作进行复制,而无需调用构造函数或者赋值运算符。
- 基本类型:
支持的操作
- 原子读写:
load()/store() - 自增自减:
++、--、fetch_add()、fetch_sub() - 比较并交换(CAS):
compare_exchange_strong()/compare_exchange_weak()
- 原子读写:
使用注意事项
std::atomic不支持拷贝赋值(复制是被禁用的)。- 默认是顺序一致性(
memory_order_seq_cst),内存可见性是有保障的。 - 支持内存顺序优化(高级用法),包括
memory_order_relaxed、acquire、release等。
volatile 能干什么?它和 atomic 是一回事吗?
特别注意,volatile 与 std::atomic 不是一回事!volatile 在 C++ 里的作用和你想的不太一样。
| 功能 | std::atomic | volatile |
|---|---|---|
| 保证原子性? | ✅ 是 | ❌ 否 |
| 保证内存可见性? | ✅ 是 | ❌ 否 |
| 保证编译器不优化读写? | ✅是(控制更强) | ✅是(仅编译器层面) |
| 多线程同步安全? | ✅ 是 | ❌ 否 |
- 结论:
volatile只告诉编译器 “不要优化这个变量的访问”。volatile不会保证线程间同步,不会保证缓存刷新,不会保证乱序执行的控制。- 所以,在 C++ 多线程编程里,
volatile基本没用(少数平台 / 驱动除外)。
atomic 原子类型的使用
这里将演示如何使用基于 CAS 操作的 atomic 原子类型,比如 atomic_int 和 atomic_bool。
1 |
|
程序运行的输出结果如下:
1 | _count = 1000 |
