[C++高频精进] 并发编程:线程同步

核心要点速览

  • 数据竞争:多线程并发读写共享资源(至少一个写操作)的未定义行为
  • 互斥锁:std::mutex(基础)、lock_guard(RAII 推荐)、unique_lock(灵活,配条件变量)
  • 条件变量:wait()(阻塞 + 释锁)、notify_one()/notify_all(),需配互斥锁 + 谓词(解虚假唤醒)
  • 原子操作:std::atomic(硬件级原子性,无锁),核心内存序(relaxed/acquire/release/seq_cst)
  • 读写锁:std::shared_mutex(读共享、写独占),优化读多写少场景
  • 常见问题:死锁(固定加锁顺序避免)、虚假唤醒(谓词检查解决)、活锁 / 饥饿(延迟 / 公平锁缓解)

一、线程同步的目标

  1. 保证数据一致性:避免多线程读写共享资源导致的结果不可预测。
  2. 控制执行顺序:确保线程按业务逻辑要求的顺序执行(如生产者先生产,消费者后消费)。

二、同步机制

1. 互斥锁:独占式临界区访问

  • 原理:通过 “加锁 - 操作 - 解锁”,保证同一时间仅一个线程进入临界区(访问共享资源的代码段)。
锁类型与特性
锁类型特性适用场景
std::mutex基础互斥锁,不可递归加锁简单独占访问,配合 RAII 锁使用
std::lock_guardRAII 自动管理(构造加锁、析构解锁)无需手动控制锁,避免漏解锁 / 异常
std::unique_lock灵活控制(可延迟加锁、手动解锁)配合条件变量,需动态控制锁状态
std::recursive_mutex允许同一线程重复加锁递归函数访问临界区(不推荐,易藏错)
std::timed_mutex支持超时尝试加锁(try_lock_for避免无限阻塞,需限时获取锁场景
注意事项
  • std::mutexlock()/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. 原子变量:无锁同步

  • 原理:基于硬件级原子操作(如 CPULOCK指令),保证读写操作不可分割,无需加锁即可避免数据竞争。
特性
  • 非阻塞:不会导致线程挂起,性能远高于互斥锁(高并发场景优先)。
  • 支持操作:仅简单操作(++/--/load/store/exchange),复杂逻辑仍需锁。
  • 内存序:
    • memory_order_relaxed:仅保证操作原子性,不保证内存可见性 / 顺序。
    • memory_order_acquire/release:读(acquire)后可见所有写(release)操作,保证顺序。
    • memory_order_seq_cst:默认,最强保证(全局顺序一致),性能略低。
区别
  • std::atomic vs volatile
    • volatile:仅禁止编译器优化(每次从内存读取),不保证原子性(多线程读写仍竞争)。
    • atomic:保证原子性 + 内存可见性,是线程安全的。

4. 读写锁:读多写少优化

  • 原理:区分读 / 写操作,允许多线程同时读(共享锁),仅允许单线程写(独占锁)。
  • 标准库实现:std::shared_mutex(C++17+),配合std::shared_lock(读锁)、std::unique_lock(写锁)。
  • 适用场景:读操作远多于写操作(如缓存、配置读取),避免读操作互相阻塞。

三、同步机制对比表

机制原理优点缺点适用场景
互斥锁独占临界区适用所有场景,实现简单阻塞,高并发性能损耗大复杂临界区(多步操作)
条件变量等待 - 通知机制解决执行顺序,避免忙等需配互斥锁,逻辑复杂生产者 - 消费者、线程协作
原子变量硬件级原子操作非阻塞,性能极高仅支持简单操作计数器、标记位、简单共享数据
读写锁读共享、写独占优化读多写少场景性能写操作可能饥饿,实现复杂缓存、配置等读多写少场景

四、同步常见问题与解决方案

1. 死锁

  • 定义:多个线程互相等待对方释放锁,导致永久阻塞。
  • 避免方案:
    1. 固定加锁顺序:所有线程按相同顺序加锁(如先mtx1mtx2)。
    2. 批量加锁:用std::lock(mtx1, mtx2)一次性加锁所有需要的锁。
    3. 限时等待:用try_lock_for/try_lock_until,超时则释放已持锁。
    4. 减少锁粒度:缩小临界区范围,缩短持有锁的时间。

2. 活锁

  • 定义:线程不断释放 / 重试获取锁,因相互冲突始终无法推进(看似活跃实则无进展)。
  • 解决方案:重试前随机延迟(降低冲突概率),或引入优先级机制。

3. 饥饿

  • 定义:部分线程长期无法获取资源(如低优先级线程被高优先级线程抢占)。
  • 避免方案:使用公平锁(按请求顺序分配锁),限制高优先级线程执行时长。

五、问答

1. 条件变量为什么必须配合互斥锁使用?

  1. 检查条件时加锁,防止检查期间条件被其他线程修改(保证条件判断原子性)。
  2. wait()阻塞前释放锁,允许其他线程修改条件(避免线程间互相阻塞)。
  3. 唤醒后重新加锁,确保后续操作基于最新的条件状态(保证数据一致性)。

2. 如何解决条件变量的虚假唤醒?

wait()的第二个参数传入条件谓词(如wait(lock, []{ return flag; })),唤醒后再次检查条件,只有条件为true才继续执行,否则重新阻塞。

3. std::atomicvolatile的区别?

volatile仅禁止编译器优化,保证变量每次从内存读取,但不保证原子性(多线程读写仍会竞争);std::atomic既保证原子性(操作不可分割),又保证内存可见性(一个线程的修改对其他线程立即可见),是线程安全的。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值