多线程编程:从单核到多核的进化之路

说实话,我刚入行那会儿,C++ 的多线程还是个「野路子」——要么用操作系统 API,要么靠第三方库。直到 C++11 正式把 std::threadstd::mutex 这些东西纳入标准库,我才觉得:嗯,这才像个现代语言的样子。

这一章,咱们就聊聊 C++ 多线程编程的核心武器。我会结合自己踩过的坑,把 std::threadstd::mutex、条件变量、std::async 这些家伙讲明白。

1. std::thread:开启新战场

创建线程,说白了就是告诉 CPU:「嘿,这段代码你帮我另开一条流水线跑。」

#include <thread>
#include <iostream>

void worker(int id) {
    std::cout << "线程 " << id << " 开始干活\n";
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);

    t1.join();  // 等 t1 结束
    t2.join();  // 等 t2 结束
    return 0;
}

这里有个细节:join() 是阻塞等待。如果你不调用 join()detach(),程序退出时会直接 terminate()。我曾经在项目里漏掉一个 join(),结果线上服务莫名其妙崩溃——排查了半天才发现是线程没回收。

注意:线程对象销毁前,必须明确选择 join()detach()。否则程序会异常终止。

2. std::mutex 与 std::lock_guard:锁住共享资源

多线程最怕什么?数据竞争。两个线程同时写一个变量,结果就乱套了。我见过一个经典 bug:两个线程同时更新一个计数器,最后结果比预期少了 30%。

解决方案?加锁。

#include <mutex>
#include <thread>

std::mutex mtx;
int counter = 0;

void increment() {
    std::lock_guard<std::mutex> lock(mtx);  // 自动加锁,离开作用域自动解锁
    ++counter;
}

std::lock_guard 是个好东西——它遵循 RAII 原则,构造时加锁,析构时解锁。你不用担心忘记 unlock,也不会因为异常导致死锁。

我的习惯:能用 std::lock_guard 就别手动 lock()/unlock()。少写代码,少犯错。

3. 条件变量:让线程学会等待

有时候,线程需要等某个条件成立再继续。比如生产者-消费者模型:消费者得等生产者放好东西才能取。

条件变量就是干这个的。

#include <condition_variable>
#include <mutex>
#include <queue>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return !data_queue.empty(); });  // 等队列非空
    int val = data_queue.front();
    data_queue.pop();
    // 处理 val ...
}

注意这里用的是 std::unique_lock 而不是 lock_guard。为什么?因为 wait() 内部需要临时解锁,再重新加锁——unique_lock 支持这种操作,lock_guard 不行。

核心要点:条件变量的 wait() 必须配合 unique_lock,并且最好用带谓词的重载版本,避免虚假唤醒。

4. std::async 与 std::future:异步任务的优雅写法

说实话,手动管理线程有时候挺烦的。你想想看,我就想异步执行一个函数,拿个返回值,还得自己搞线程、传参、处理异常……

std::async 就是来解放你的。

#include <future>
#include <iostream>

int compute(int x) {
    return x * x;
}

int main() {
    std::future<int> result = std::async(std::launch::async, compute, 10);
    std::cout << "结果: " << result.get() << std::endl;
    return 0;
}

std::async 会自动选择一个线程(或者在当前线程同步执行)来运行任务。std::future 则用来获取异步结果。get() 会阻塞直到结果就绪。

我个人习惯用 std::launch::async 强制异步执行,避免某些实现偷懒用同步方式。

5. 线程安全与数据竞争:避坑指南

数据竞争是未定义行为。编译器优化、CPU 乱序执行、缓存不一致……这些因素会让你的程序行为完全不可预测。

我曾经遇到过一个诡异的 bug:一个 bool 标志位,主线程设置它为 true,工作线程检查它。结果工作线程永远看不到 true。为什么?因为编译器把那个变量的读取优化掉了——它觉得这个变量没被修改过。

解决方案:用 std::atomic 或者加锁。

std::atomic<bool> ready{false};

void worker() {
    while (!ready.load()) {
        // 忙等待
    }
    // 干活...
}

void set_ready() {
    ready.store(true);
}
警告:不要用 volatile 做线程同步。volatile 只告诉编译器不要优化,但不保证内存序。C++ 多线程同步请用 std::atomic 或互斥量。

知识体系总览

下面这张图帮你理清本章的核心脉络:

C++ 多线程编程 线程管理 • std::thread 创建/join/detach • 线程参数传递 • 线程生命周期管理 同步机制 • std::mutex / lock_guard • std::unique_lock • 条件变量 (condition_variable) 异步任务 • std::async 启动异步任务 • std::future 获取结果 • std::promise 手动设置 线程安全与数据竞争 • 数据竞争 = 未定义行为 • std::atomic 原子操作 • 内存序 (memory_order) 控制 • 避免死锁:固定加锁顺序 / std::lock

总结一下

多线程编程,说白了就是「分工 + 协作」。分工靠 std::threadstd::async,协作靠 std::mutex、条件变量和 std::atomic

我最后给你三个建议:

  • 能用 std::async 就别手动创建线程——少操心,少出错。
  • 加锁范围尽量小——锁住关键代码就行,别锁整个函数。
  • 多写测试——多线程 bug 很难复现,但一旦出现就是大问题。

嗯,这一章就到这里。记住:多线程不是魔法,是工程。你踩过的坑越多,写出来的代码就越稳。

公众号:蓝海资料掘金营,微信deep3321