目录

1. std::thread::join() vs std::thread::detach()

2. RAII 风格的锁:std::lock_guard 与 std::unique_lock

std::lock_guard

std::unique_lock

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

4. std::future / std::promise / std::async

4.1 std::future:拿结果的“票”

4.2 std::promise:主动“写入结果”的一端

4.3 std::async:一行搞定异步任务

5. 死锁与预防

5.1 死锁的四个必要条件

5.2 打破循环等待的常用手段

6. 如何检测和调试死锁

7. 原子操作与内存模型:std::atomic

8. 为什么要用 RAII 锁?


1. std::thread::join() vs std::thread::detach()

join():等待线程结束

  • 当前线程阻塞,直到子线程运行完毕。

  • 用于:

    • 需要 确认任务完成(例如要拿结果、要保证逻辑顺序)。

    • 需要在退出前确保所有线程都结束。

  • 注意:每个 std::thread 只能 join() 一次,且仅对 joinable() 的线程调用。

std::thread t(worker);
if (t.joinable()) {
    t.join(); // 等待执行完
}

detach():与主线程“脱离关系”

  • 线程变成 后台线程,主线程不再管理它。

  • 进程结束时,仍在运行的后台线程会被 强制终止

  • 常见场景:

    • 日志、监控、心跳等确实无所谓结果的后台任务。

  • 风险:

    • 被 detach 的线程 不能再 join

    • 可能访问已经销毁的对象(比如主线程退出或局部变量结束生命周期)。

std::thread t(worker);
t.detach(); // 变成守护线程

一般规则:
join() 就不要 detach();后台线程要特别小心生命周期。


2. RAII 风格的锁:std::lock_guardstd::unique_lock

std::lock_guard
  • 最简单的 RAII 锁,构造时加锁、析构时自动解锁。

  • 不可手动解锁/重新上锁,也不能延迟加锁。

  • 适合:作用域简单、不需要锁的高级操作的场景。

std::mutex m;
void f() {
    std::lock_guard<std::mutex> lk(m);
    // 临界区
} // lk 析构 -> 自动 unlock()

std::unique_lock
  • 更灵活的 RAII 锁,支持:

    • 延迟加锁 std::unique_lock<std::mutex> lk(m, std::defer_lock);

    • 手动 lock()/unlock()

    • 可移动(可从函数中返回锁)

    • 必须用 std::condition_variable 搭配 unique_lock(因为会临时释放/重新获取锁)。

std::mutex m;
void f() {
    std::unique_lock<std::mutex> lk(m);  // 构造时加锁
    // 临界区
    lk.unlock();
    // 不在锁内
    lk.lock();
}

经验:

  • 默认用 std::lock_guard(简单、安全)。

  • 需要配合 condition_variable 或手动控制加解锁时用 std::unique_lock


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

用途: 让线程 在条件满足前睡眠,由其他线程在条件变化时唤醒。避免“死循环 + 睡眠”轮询。

标准用法模式:

std::mutex m;
std::condition_variable cv;
bool ready = false;

void producer() {
    {
        std::lock_guard<std::mutex> lk(m);
        ready = true;
    }
    cv.notify_one(); // 或 notify_all()
}

void consumer() {
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{ return ready; }); // 带谓词,避免虚假唤醒
    // ready == true,安全使用共享数据
}

关键点:

  • wait() 必须传 std::unique_lock,因为它要:

    1. 暂时释放锁让其他线程修改条件;

    2. 被唤醒后重新加锁,再返回。

  • 总是使用 带谓词版本 wait(lock, predicate),避免虚假唤醒。


4. std::future / std::promise / std::async

4.1 std::future:拿结果的“票”
  • 表示一次异步计算的 结果(或异常)。

  • 通过 future.get() 获取结果(阻塞直到结果就绪,且只能调用一次)。


4.2 std::promise:主动“写入结果”的一端
  • promisefuture 配对使用:

    • promise 端:set_value()set_exception()

    • future 端:get() 阻塞等待结果

std::promise<int> p;
std::future<int> f = p.get_future();

std::thread t([&p]{
    p.set_value(42);
});

int x = f.get(); // 得到 42
t.join();

4.3 std::async:一行搞定异步任务
  • 自动创建线程或在线程池中执行(由实现决定)。

  • 返回一个 std::future,用 get() 拿结果。

std::future<int> f = std::async(std::launch::async, []{
    return 42;
});

int x = f.get(); // 阻塞直到任务完成

小结:

  • 异步执行 + 获取返回值:首选 std::async

  • 要自己控制任务启动/线程:用 promise + future


5. 死锁与预防

5.1 死锁的四个必要条件
  1. 互斥:资源一次只能被一个线程占用。

  2. 持有并等待:拿着一个锁,还在等待另一个锁。

  3. 不可抢占:锁不能强制被别的线程抢走。

  4. 循环等待:多个线程按环形次序互相等待对方的锁。

实践上常用的是:打破“循环等待”


5.2 打破循环等待的常用手段
  1. 固定加锁顺序

    • 约定所有线程按同样的顺序加锁:
      先锁 m1,再锁 m2,再锁 m3 …

    • 若需要同时锁多个 mutex,可用 std::lock()

    std::lock(m1, m2);
    std::lock_guard<std::mutex> lk1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lk2(m2, std::adopt_lock);
    
  2. std::scoped_lock(C++17 推荐)

    • 同时锁多个 mutex,自动处理加锁顺序,不会死锁

    std::scoped_lock lk(m1, m2); // 最安全、最省心
    
  3. 减少锁粒度(减小临界区)

    • 缩短持锁时间:

      • 不要在锁内做耗时操作(IO、网络、sleep 等)。

      • 处理数据前后分离:锁里只做“拷贝/取出”,再在锁外慢慢处理。

  4. 非阻塞锁 try_lock()

    • 拿不到锁就 立即返回,不进入等待队列,降低死锁概率:

    if (m.try_lock()) {
        // 成功获取
        m.unlock();
    } else {
        // 拿不到锁,选择放弃/重试
    }
    

6. 如何检测和调试死锁

典型症状:

  • 程序“卡住”但 CPU 使用率很低

  • 调试时大量线程停在 pthread_mutex_lock / futex_wait / std::mutex::lock 等位置。

  • 多个线程互相等待别人释放锁,形成环。


常用方法:

  1. 工具检测

    • Clang:-fsanitize=thread(ThreadSanitizer)

    • Visual Studio:Concurrency Visualizer

    • 部分平台有专门的死锁检测/线程分析工具。

  2. 日志追踪

    • 对每次 加锁/解锁 打日志(可带线程 id 和锁地址)。

    • 卡住时看最后一个成功加锁的人是谁,锁没释放在哪里。

  3. 超时机制

    • 使用 带超时的锁

      • std::timed_mutextry_lock_for() / try_lock_until()

      • std::unique_lock + std::condition_variable::wait_for()

    • 超时后打印报警日志,便于排查:

    std::timed_mutex m;
    if (!m.try_lock_for(std::chrono::milliseconds(100))) {
        // 认为可能有问题,记录日志
    }
    

7. 原子操作与内存模型:std::atomic

std::atomic 的特点:

  • 对单个变量的读写是 原子 的(不会被打断)。

  • 常用于:计数器、标志位、无锁队列等场景。

  • 常见类型:std::atomic<int>, std::atomic<bool>, std::atomic<void*> 等。

std::atomic<int> counter{0};

void f() {
    counter.fetch_add(1); // 原子自增
}

内存模型(简要):

  • memory_order_seq_cst默认,最强保证,最简单也最安全。

  • memory_order_acquire / memory_order_release

    • 写时 release,读时 acquire,形成“先写后读”的有序关系。

  • memory_order_relaxed:只保证原子性,不保证顺序,一般仅用于简单计数器。

经验:

  • 不熟悉内存模型时,保持使用默认 seq_cst

  • 真正需要极致性能再考虑更弱的顺序约束。


8. 为什么要用 RAII 锁?

手动 lock() / unlock() 的最大风险:

  • 在临界区内 抛异常 / 提前 return / 中途 throw,忘记 unlock(),导致锁永远不被释放 → 直接导致死锁。

RAII 风格的锁(lock_guard / unique_lock)依赖于 C++ 的 作用域与析构

void f() {
    std::lock_guard<std::mutex> lk(m);
    // 中间无论是 return 还是抛异常
} // 离开作用域,lk 自动析构 -> 自动 unlock()

经验法则:
不要在业务代码里直接写 m.lock() / m.unlock()
把锁交给 RAII 对象管理。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐