C++ 线程同步复习

C++ 线程同步

线程同步是多线程编程的核心,旨在解决数据竞争执行顺序协调 问题。本文将快速回顾基础,重点聚焦高阶用法(内存序、无锁编程、C++20 新特性等)。


第一部分:基础快速回顾

1. 互斥锁 (std::mutex)

最基础的同步原语,通过 lock()/unlock() 保护临界区。

cpp 复制代码
#include <mutex>
std::mutex mtx;

void safe_increment(int& x) {
    mtx.lock();
    ++x; // 临界区
    mtx.unlock();
}

2. 条件变量 (std::condition_variable)

用于线程间执行顺序协调 ,配合 std::unique_lock 使用。

cpp 复制代码
#include <condition_variable>
std::condition_variable cv;
bool ready = false;

void worker() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return ready; }); // 等待条件满足
    // 执行任务
}

3. 原子操作 (std::atomic)

无需锁的轻量级同步,保证操作的原子性

cpp 复制代码
#include <atomic>
std::atomic<int> counter{0};
counter.fetch_add(1); // 原子自增

第二部分:锁的高阶封装与类型

1. std::lock_guard:RAII 简单锁

自动管理锁的生命周期,不可移动、不可提前解锁

cpp 复制代码
{
    std::lock_guard<std::mutex> guard(mtx);
    // 临界区
} // 自动解锁

2. std::unique_lock:灵活锁

支持移动、提前解锁、延迟加锁,是条件变量的标配。

cpp 复制代码
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟加锁
// ... 其他操作 ...
lock.lock(); // 手动加锁
lock.unlock(); // 提前解锁

3. std::shared_mutex + std::shared_lock:读写锁

实现多读单写(Readers-Writer Lock),读操作共享锁,写操作独占锁。

cpp 复制代码
#include <shared_mutex>
std::shared_mutex rw_mtx;

void read() {
    std::shared_lock<std::shared_mutex> lock(rw_mtx); // 共享锁(读)
    // 读操作
}

void write() {
    std::unique_lock<std::shared_mutex> lock(rw_mtx); // 独占锁(写)
    // 写操作
}

4. std::scoped_lock(C++17):多锁同时加锁

避免死锁的利器,可同时对多个互斥锁原子性加锁。

cpp 复制代码
std::mutex m1, m2;
{
    std::scoped_lock lock(m1, m2); // 同时加锁 m1 和 m2,自动避免死锁
    // 临界区
} // 自动解锁

第三部分:条件变量的深度解析

1. 虚假唤醒(Spurious Wakeup)

条件变量可能被操作系统虚假唤醒 ,必须用 while 循环检查条件。

cpp 复制代码
cv.wait(lock, []{ return ready; }); // 等价于:
// while (!ready) { cv.wait(lock); }

2. 生产者-消费者模型(标准实现)

cpp 复制代码
std::queue<int> q;
const int MAX_SIZE = 10;

void producer(int val) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return q.size() < MAX_SIZE; }); // 队列不满
    q.push(val);
    cv.notify_one(); // 通知消费者
}

int consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return !q.empty(); }); // 队列不空
    int val = q.front();
    q.pop();
    cv.notify_one(); // 通知生产者
    return val;
}

第四部分:原子操作与内存序(核心高阶)

std::atomic 的默认内存序是 std::memory_order_seq_cst(顺序一致性),但通过调整内存序可优化性能

1. 六种内存序

内存序 含义 适用场景
memory_order_relaxed 仅保证原子性,不保证顺序 计数器、统计信息
memory_order_acquire 读操作:阻止后续读写重排到本操作之前 消费者(读取数据)
memory_order_release 写操作:阻止前面读写重排到本操作之后 生产者(写入数据)
memory_order_acq_rel 同时具有 acquirerelease 语义 读写混合操作
memory_order_seq_cst 默认,全局顺序一致性(所有线程看到一致的操作顺序) 最严格、最安全(性能稍低)

2. 示例:Acquire-Release 实现生产者-消费者

cpp 复制代码
std::atomic<int> data{0};
std::atomic<bool> ready{false};

void producer() {
    data.store(42, std::memory_order_relaxed); // 写入数据
    ready.store(true, std::memory_order_release); // 释放:确保 data 先写入
}

void consumer() {
    while (!ready.load(std::memory_order_acquire)); // 获取:等待 ready
    assert(data.load(std::memory_order_relaxed) == 42); // 保证读到 data
}

3. 内存序选择原则

  • 优先用 memory_order_seq_cst(默认),确保正确性;
  • 性能瓶颈时,对生产者-消费者 模型用 acquire-release
  • 仅对独立计数器用 memory_order_relaxed

第五部分:无锁编程进阶

无锁编程(Lock-Free)通过原子操作避免锁开销,但复杂度极高,仅在性能关键场景使用。

1. CAS(Compare-And-Swap)操作

无锁编程的核心,通过 compare_exchange_weak/compare_exchange_strong 实现。

cpp 复制代码
std::atomic<int> val{0};

void lock_free_increment() {
    int old_val = val.load(std::memory_order_relaxed);
    // 循环尝试 CAS,直到成功
    while (!val.compare_exchange_weak(old_val, old_val + 1,
                                        std::memory_order_acq_rel));
}

2. 无锁队列(解决 ABA 问题)

ABA 问题:值从 A→B→A,CAS 会误判为未改变。解决方案:带版本号的指针

cpp 复制代码
template<typename T>
struct Node {
    T data;
    Node* next;
    Node(T val) : data(val), next(nullptr) {}
};

std::atomic<Node<int>*> head{nullptr};

void lock_free_push(int val) {
    Node<int>* new_node = new Node<int>(val);
    new_node->next = head.load(std::memory_order_relaxed);
    // CAS 更新头指针
    while (!head.compare_exchange_weak(new_node->next, new_node,
                                        std::memory_order_acq_rel));
}

3. 无锁编程的挑战

  • ABA 问题(用版本号或 std::atomic_shared_ptr 解决);
  • 内存回收(无锁环境下难以安全释放内存,需用 Hazard Pointers 或 Epoch-Based Reclamation);
  • 可移植性差(依赖硬件原子指令支持)。

第六部分:C++20 线程同步新特性

1. std::counting_semaphore:信号量

控制同时访问资源的线程数量。

cpp 复制代码
#include <semaphore>
std::counting_semaphore<3> sem(3); // 最多 3 个线程同时访问

void worker() {
    sem.acquire(); // 获取信号量
    // 访问资源
    sem.release(); // 释放信号量
}

2. std::latch:单次使用的线程屏障

等待 N 个线程到达,不可重用

cpp 复制代码
#include <latch>
std::latch work_latch(5); // 等待 5 个线程

void worker() {
    // 执行任务
    work_latch.count_down(); // 计数减 1
    work_latch.wait(); // 等待计数为 0
}

3. std::barrier:可重用的线程屏障

等待 N 个线程到达,到达后可执行回调,可重用

cpp 复制代码
#include <barrier>
std::barrier sync_barrier(5, []{ /* 所有线程到达后执行的回调 */ });

void worker() {
    for (int i = 0; i < 10; ++i) {
        // 执行第 i 轮任务
        sync_barrier.arrive_and_wait(); // 等待其他线程
    }
}

第七部分:性能优化与最佳实践

1. 锁粒度的选择

  • 粗粒度锁:整个操作加一把锁(简单,但竞争大);
  • 细粒度锁:拆分临界区,用多把锁(复杂,但竞争小)。

2. 死锁避免策略

  • 按固定顺序加锁(如按锁的地址从小到大);
  • std::scoped_lock(C++17)或 std::lock 同时加锁;
  • 避免在持有锁时调用用户代码(防止回调中再次加锁)。

3. 减少锁竞争的技巧

  • 线程局部存储thread_local)避免共享;
  • 读写锁std::shared_mutex)优化多读场景;
  • 无锁数据结构 (如 boost::lockfree)替代锁。

4. 调试与分析工具

  • ThreadSanitizer(TSan):检测数据竞争与死锁;
  • perf:分析性能瓶颈;
  • gdb :调试多线程程序(info threadsthread apply)。

相关推荐
Full Stack Developme2 小时前
Hutool EnumUtil 教程
开发语言·windows·python
XMYX-02 小时前
18 - Go 等待协程:WaitGroup 使用与坑
开发语言·golang
feifeigo1232 小时前
基于遗传算法的矩形排样MATLAB实现
开发语言·matlab
他是龙5512 小时前
65:JS安全&浏览器插件&工具箱等
开发语言·javascript·安全
csbysj20202 小时前
Rust 输出到命令行
开发语言
likerhood2 小时前
Java 中的 `clone()` 与 `Cloneable` 接口详解
java·开发语言·python
Adellle2 小时前
Java 异步回调
java·开发语言·多线程
海寻山2 小时前
Java常用API详解(二):集合类API(ArrayList/HashMap/HashSet)实战,一篇吃透
开发语言·python
XMYX-02 小时前
19 - Go 并发限制:限流与控制并发数
开发语言·golang