Skip to content

线程基础:thread / mutex / condition_variable / 生产者消费者

一、std::thread 使用要点

cpp
#include <thread>
void f(int x) { /*...*/ }
std::thread t(f, 1);
t.join();            // 等待;一定要 join 或 detach
// 不 join 不 detach → 析构 std::thread 会 std::terminate 💥
  • 线程对象在析构前必须 join 或 detach,否则 terminate。
  • 传参默认拷贝/按值;想传引用用 std::ref,否则拷贝到线程。

二、互斥锁 mutex + RAII

cpp
std::mutex m;
int cnt = 0;
void inc() {
    std::lock_guard<std::mutex> lk(m);   // 构造加锁,析构解锁(离开作用域自动解锁)
    ++cnt;
}
  • lock_guard:简单 RAII,加锁解锁全程作用域。
  • unique_lock:更灵活——可延迟加锁、临时 unlock(配合 condition_variable)、移动。代价略高。
  • 避免新裸 lock/unlock 手工管理(异常路径忘解锁)。

避免的误区

  • 在循环里反复 lock/unlock。
  • 锁的粒度:持锁时间过长会串行化,损害并发。
  • 加锁顺序不一致 → 死锁(见死锁篇)。

三、条件变量 condition_variable(重要)

等待某个条件发生再继续。必须与 mutex 配合(阻塞期间要释放锁,允许别人改条件)。

cpp
std::mutex m;
std::condition_variable cv;
std::queue<int> q;

// 生产者 push
{
    std::lock_guard<std::mutex> lk(m);
    q.push(x);
}
cv.notify_one();   // 唤醒一个等待者

// 消费者
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{ return !q.empty(); });   // 带谓词版:返回时才继续
int v = q.front(); q.pop();

为什么 wait 必须配 unique_lock 而不是 lock_guard? 因为 wait 内部要临时释放锁、阻塞、被唤醒后重新加锁;unique_lock 支持 unlock/lock 而 lock_guard 不行。

虚假唤醒(spurious wakeup)/ 如何避免? 等待者可能被无意义唤醒,必须用带谓词的 wait(或 while 循环检查条件),条件不满足继续睡。带谓词 wait 等价于 while(!pred()) cv.wait(lk);

  • notify_one 唤醒一个;notify_all 唤醒全部(多个消费者用)。
  • 条件变量底层依赖 OS 的 futex/wait-queue。

四、future / promise / async

一次性传值 / 异步调用:

cpp
std::future<int> fu = std::async(std::launch::async, []{ return 42; });
int r = fu.get();          // 阻塞直到结果就绪

std::promise<int> p;
std::future<int> f = p.get_future();
std::thread t([&]{ p.set_value(100); });
int v = f.get();           // 100
t.join();
  • std::async 可指定 launch::async(新线程)或 deferred(惰性)。
  • future 只能 get 一次;多个方等待一个结果用 shared_future。

五、生产-消费者模型(手写高频)

cpp
class SafeQueue {
public:
    void push(int v) {
        std::unique_lock<std::mutex> lk(m_);
        q_.push(v);
        cv_.notify_one();
    }
    int pop() {                    // 阻塞版
        std::unique_lock<std::mutex> lk(m_);
        cv_.wait(lk, [&]{ return !q_.empty() || stop_; });
        if (q_.empty()) return -1; // 停止信号
        int v = q_.front(); q_.pop();
        return v;
    }
    void shutdown() { std::unique_lock<std::mutex> lk(m_); stop_=true; cv_.notify_all(); }
private:
    std::queue<int> q_;
    std::mutex m_;
    std::condition_variable cv_;
    bool stop_ = false;
};

考察点:谓词 wait(防虚假唤醒 / 防 stop 时卡死)、notify 时机(push 后唤醒)、停止机制(notify_all + 停止标志使等待者退出)、锁粒度(pop/push 均加锁保护 queue)。

六、TSan / 常见并发排查

  • -fsanitize=thread(ThreadSanitizer)检测数据竞争。
  • 崩在随机位置、偶发值错乱是典型数据竞争症状,先在 getter/setter 排竞态。
  • 锁尽量用 RAII;共享可变权交给“单一写者 + future/mutex”,减少共享。

七、易错点速记

  1. lock_guard 不能配 condition_variable.wait。
  2. notify 前应释放锁(避免唤醒了又立刻阻塞)。
  3. 别用 volatile 做线程同步(不保证原子与内存序,见 atomic 篇)。
  4. std::atomic<int> 重载 ++ 可以,但复合操作如 a=a+1 若先读后写可能被拆?—— fetch_add 原子;表达式 a=a+1 对 atomic 内部是原子的 operator+= 形式,没问题;困难在「读-改-写需要自主原子化」用 CAS。

C++ 面试八股 · VitePress 版