核心要点速览
- 数据竞争:多线程并发读写共享资源(至少一个写操作)的未定义行为
- 互斥锁:
std::mutex(基础)、lock_guard(RAII 推荐)、unique_lock(灵活,配条件变量) - 条件变量:
wait()(阻塞 + 释锁)、notify_one()/notify_all(),需配互斥锁 + 谓词(解虚假唤醒) - 原子操作:
std::atomic(硬件级原子性,无锁),核心内存序(relaxed/acquire/release/seq_cst) - 读写锁:
std::shared_mutex(读共享、写独占),优化读多写少场景 - 常见问题:死锁(固定加锁顺序避免)、虚假唤醒(谓词检查解决)、活锁 / 饥饿(延迟 / 公平锁缓解)
一、线程同步的目标
- 保证数据一致性:避免多线程读写共享资源导致的结果不可预测。
- 控制执行顺序:确保线程按业务逻辑要求的顺序执行(如生产者先生产,消费者后消费)。
二、同步机制
1. 互斥锁:独占式临界区访问
- 原理:通过 “加锁 - 操作 - 解锁”,保证同一时间仅一个线程进入临界区(访问共享资源的代码段)。
锁类型与特性
| 锁类型 | 特性 | 适用场景 |
|---|---|---|
std::mutex | 基础互斥锁,不可递归加锁 | 简单独占访问,配合 RAII 锁使用 |
std::lock_guard | RAII 自动管理(构造加锁、析构解锁) | 无需手动控制锁,避免漏解锁 / 异常 |
std::unique_lock | 灵活控制(可延迟加锁、手动解锁) | 配合条件变量,需动态控制锁状态 |
std::recursive_mutex | 允许同一线程重复加锁 | 递归函数访问临界区(不推荐,易藏错) |
std::timed_mutex | 支持超时尝试加锁(try_lock_for) | 避免无限阻塞,需限时获取锁场景 |
注意事项
std::mutex的lock()/unlock()必须成对出现,否则引发死锁;优先用lock_guard/unique_lock(RAII 安全)。- 不可递归加锁普通
std::mutex(同一线程多次lock()会死锁)。
2. 条件变量:线程间等待 - 通知
- 原理:实现线程协作,让线程在条件不满足时阻塞(释放 CPU,避免忙等),条件满足时被唤醒。
接口与规则
wait(lock, predicate):先释放锁→阻塞等待→被唤醒后重新加锁→检查谓词,为true则继续,否则再次阻塞(解决虚假唤醒)。notify_one():唤醒一个等待线程(避免资源浪费,优先使用)。notify_all():唤醒所有等待线程(适合多个线程需响应条件变化)。
要点
- 必须与
std::unique_lock配合:wait()需手动控制锁的释放与重获取。 - 必须用谓词检查条件:避免操作系统虚假唤醒(无
notify时的唤醒)。 - 依赖互斥锁的原因:保证条件判断和修改的原子性(防止检查期间条件被篡改)。
3. 原子变量:无锁同步
- 原理:基于硬件级原子操作(如 CPU
LOCK指令),保证读写操作不可分割,无需加锁即可避免数据竞争。
特性
- 非阻塞:不会导致线程挂起,性能远高于互斥锁(高并发场景优先)。
- 支持操作:仅简单操作(
++/--/load/store/exchange),复杂逻辑仍需锁。 - 内存序:
memory_order_relaxed:仅保证操作原子性,不保证内存可见性 / 顺序。memory_order_acquire/release:读(acquire)后可见所有写(release)操作,保证顺序。memory_order_seq_cst:默认,最强保证(全局顺序一致),性能略低。
区别
std::atomicvsvolatile:volatile:仅禁止编译器优化(每次从内存读取),不保证原子性(多线程读写仍竞争)。atomic:保证原子性 + 内存可见性,是线程安全的。
4. 读写锁:读多写少优化
- 原理:区分读 / 写操作,允许多线程同时读(共享锁),仅允许单线程写(独占锁)。
- 标准库实现:
std::shared_mutex(C++17+),配合std::shared_lock(读锁)、std::unique_lock(写锁)。 - 适用场景:读操作远多于写操作(如缓存、配置读取),避免读操作互相阻塞。
三、同步机制对比表
| 机制 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 互斥锁 | 独占临界区 | 适用所有场景,实现简单 | 阻塞,高并发性能损耗大 | 复杂临界区(多步操作) |
| 条件变量 | 等待 - 通知机制 | 解决执行顺序,避免忙等 | 需配互斥锁,逻辑复杂 | 生产者 - 消费者、线程协作 |
| 原子变量 | 硬件级原子操作 | 非阻塞,性能极高 | 仅支持简单操作 | 计数器、标记位、简单共享数据 |
| 读写锁 | 读共享、写独占 | 优化读多写少场景性能 | 写操作可能饥饿,实现复杂 | 缓存、配置等读多写少场景 |
四、同步常见问题与解决方案
1. 死锁
- 定义:多个线程互相等待对方释放锁,导致永久阻塞。
- 避免方案:
- 固定加锁顺序:所有线程按相同顺序加锁(如先
mtx1后mtx2)。 - 批量加锁:用
std::lock(mtx1, mtx2)一次性加锁所有需要的锁。 - 限时等待:用
try_lock_for/try_lock_until,超时则释放已持锁。 - 减少锁粒度:缩小临界区范围,缩短持有锁的时间。
- 固定加锁顺序:所有线程按相同顺序加锁(如先
2. 活锁
- 定义:线程不断释放 / 重试获取锁,因相互冲突始终无法推进(看似活跃实则无进展)。
- 解决方案:重试前随机延迟(降低冲突概率),或引入优先级机制。
3. 饥饿
- 定义:部分线程长期无法获取资源(如低优先级线程被高优先级线程抢占)。
- 避免方案:使用公平锁(按请求顺序分配锁),限制高优先级线程执行时长。
五、问答
1. 条件变量为什么必须配合互斥锁使用?
- 检查条件时加锁,防止检查期间条件被其他线程修改(保证条件判断原子性)。
wait()阻塞前释放锁,允许其他线程修改条件(避免线程间互相阻塞)。- 唤醒后重新加锁,确保后续操作基于最新的条件状态(保证数据一致性)。
2. 如何解决条件变量的虚假唤醒?
在wait()的第二个参数传入条件谓词(如wait(lock, []{ return flag; })),唤醒后再次检查条件,只有条件为true才继续执行,否则重新阻塞。
3. std::atomic和volatile的区别?
volatile仅禁止编译器优化,保证变量每次从内存读取,但不保证原子性(多线程读写仍会竞争);std::atomic既保证原子性(操作不可分割),又保证内存可见性(一个线程的修改对其他线程立即可见),是线程安全的。
782

被折叠的 条评论
为什么被折叠?



