在C++11之后,标准库正式纳入了跨平台的多线程支持,开发者不再需要直接依赖 pthread 或 Windows API。核心组件包括 std::thread、std::mutex、std::condition_variable、std::async 以及 std::future,它们位于 <thread>、<mutex>、<future> 等头文件中。理解这些组件的语义差异,是编写正确并发程序的前提。

一、std::thread 的基本使用
std::thread 是最基础的线程抽象,构造时传入可调用对象即可启动新线程。需要注意的是,线程对象销毁前必须调用 join() 或 detach(),否则程序会调用 std::terminate 终止。
下面的示例演示了如何启动一个打印数字的线程,并在主线程中等待它结束:
#include <iostream>
#include <thread>
void print_numbers(int n) {
for (int i = 0; i < n; ++i) {
std::cout << i << " ";
}
std::cout << std::endl;
}
int main() {
std::thread t(print_numbers, 5);
t.join(); // 阻塞主线程,直到 t 执行完毕
return 0;
}
如果改用 detach(),线程会在后台独立运行,主线程不再能对其进行 join,此时必须确保线程访问的变量生命周期长于线程本身,否则会产生悬空引用。多数业务场景中,优先使用 join 并通过 RAII 封装来管理生命周期更稳妥。
1.1 线程参数的传递陷阱
std::thread 默认以值拷贝方式传递参数,若想传递引用需使用 std::ref 包装。错误用法会导致编译器尝试拷贝不可拷贝对象或产生非预期的值拷贝。
#include <thread>
#include <iostream>
void add_one(int& x) {
x += 1;
}
int main() {
int value = 0;
std::thread t(add_one, std::ref(value));
t.join();
std::cout << value << std::endl; // 输出 1
return 0;
}
上述代码若去掉 std::ref,则无法通过编译,因为线程构造函数不会自动推导引用语义。这是初学者经常踩到的第一个坑。
二、用 mutex 保护共享数据
当多个线程同时读写同一变量,就会发生数据竞争。std::mutex 提供了互斥锁,配合 std::lock_guard 可保证同一时刻只有一个线程进入临界区。
下面模拟两个线程对全局计数器累加,若不加锁结果将不可预测:
#include <thread>
#include <mutex>
#include <iostream>
int counter = 0;
std::mutex mtx;
void worker(int times) {
for (int i = 0; i < times; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}
int main() {
std::thread a(worker, 10000);
std::thread b(worker, 10000);
a.join();
b.join();
std::cout << "counter = " << counter << std::endl; // 稳定输出 20000
return 0;
}
std::lock_guard 在构造时加锁、析构时解锁,即使临界区抛出异常也能正确释放,比手动调用 lock/unlock 安全得多。若需要更灵活的控制,可使用 std::unique_lock,它支持延迟加锁与条件变量协作。
2.1 避免死锁的建议
当多个互斥量必须同时持有,应使用 std::lock 一次性锁定,避免不同线程以不同顺序加锁造成死锁。此外,尽量缩小锁的粒度,只在必要代码段加锁,可显著提升并发度。
三、std::async 与 std::future
std::async 是对线程更高层的封装,它返回一个 std::future 对象,用于在未来获取异步任务的结果。相比直接使用 std::thread,async 让返回值传递和异常处理更自然。
以下示例计算两数之和,并通过 future 取回结果:
#include <future>
#include <iostream>
int add(int a, int b) {
return a + b;
}
int main() {
std::future<int> result = std::async(std::launch::async, add, 3, 4);
// 此处主线程可做其他事
int sum = result.get(); // 阻塞直至任务完成并取回值
std::cout << "sum = " << sum << std::endl;
return 0;
}
std::launch::async 强制开启新线程;若使用 std::launch::deferred,任务会延迟到调用 get() 时才同步执行。忽略启动策略参数时,具体行为由实现决定,可能复用线程池也可能新建线程,因此显式指定策略更可控。
3.1 future 的共享与多次获取
普通 std::future 只能调用一次 get(),再次调用会抛异常。若需多个线程等待同一结果,可使用 std::shared_future,它通过拷贝共享状态,允许安全多次读取。
四、条件变量协调线程节奏
std::condition_variable 常与 mutex 配合,用于线程间通知。典型场景是生产者-消费者模型:消费者在队列为空时等待,生产者放入数据后唤醒消费者。
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <iostream>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data;
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !data.empty(); });
int v = data.front();
data.pop();
std::cout << "got " << v << std::endl;
}
void producer() {
{
std::lock_guard<std::mutex> lock(mtx);
data.push(42);
}
cv.notify_one();
}
int main() {
std::thread c(consumer);
std::thread p(producer);
c.join();
p.join();
return 0;
}
cv.wait 会自动释放锁并阻塞,被唤醒后重新加锁并判断谓词,这能防止虚假唤醒带来的逻辑错误。注意 notify 应在释放锁之前或之后均可,但一般先修改共享状态再通知,可减少上下文切换。
五、总结与选型建议
对于简单后台任务,std::thread 足够直观,但需手动管理 join;若关注返回值与异常传播,优先选 std::async;涉及共享状态保护时,mutex 与 condition_variable 是基础保障。现代 C++ 还提供了 std::atomic 用于无锁原子操作,以及并行算法等高级特性,可在理解标准库组件后进一步深入。
实际工程中,建议将线程管理封装为任务队列或线程池,避免频繁创建销毁线程的开销,同时降低直接使用底层 API 导致的并发缺陷风险。
C++_threadstd_asyncmutex修改时间:2026-08-10 03:21:30