多线程编程的核心不是简单调用 std::thread 启动几个任务,而是保证线程在任意交织执行下都不破坏共享状态。若不理解线程生命周期与同步机制,程序可能在低并发时表现正常,一旦负载上升就出现偶发错误甚至崩溃。C++11 引入的标准线程库提供了 std::thread、std::mutex、std::condition_variable、std::atomic 等组件,使得 C++ 开发者可以摆脱对平台 API 的直接依赖,构建跨平台的并发程序。本文将围绕线程创建、同步策略以及异步任务管理三个层面,分析 C++ 多线程编程的常见写法与进阶思路。

需要提前说明的是,多线程并不总是带来性能提升。线程切换、锁竞争和缓存一致性开销可能让程序比单线程更慢。因此,写多线程代码前应先确认任务是否适合并行,并优先考虑能否通过异步任务、线程池等更高层抽象降低复杂度。
一、线程创建与生命周期:std::thread 的关键细节
std::thread 在构造时立即启动线程,并在内部保存线程句柄。一个线程对象只能关联一个执行线程,不能复制,只能移动。join 会阻塞调用线程直到目标线程结束,而 detach 则将线程从 std::thread 对象中分离,让它独立运行。值得注意的是,如果一个 std::thread 对象处于 joinable 状态却在析构时没有 join 或 detach,程序会调用 std::terminate 直接终止。这是很多错误写法容易忽略的地方。
下面是一个创建多个工作线程并等待结束的例子:
#include <iostream>
#include <thread>
#include <vector>
void worker(int id) {
std::cout << "thread " << id << " running\n";
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(worker, i);
}
for (auto& t : threads) {
if (t.joinable()) {
t.join();
}
}
return 0;
}
当线程函数需要传入引用参数时,直接写 std::thread t(func, ref) 会失败,因为 std::thread 默认会复制参数,再以右值形式传递给可调用对象。要传递引用必须使用 std::ref。对于 detach 的线程,尤其要保证引用指向的对象生命周期覆盖线程运行过程,否则会产生悬垂引用。下面的代码展示了 std::ref 的正确用法:
#include <thread>
#include <iostream>
void update(int& value) {
value += 10;
}
int main() {
int total = 0;
std::thread t(update, std::ref(total));
t.join();
std::cout << "total = " << total << "\n";
return 0;
}
由于 std::thread 是移动语义类型,可以用 std::vector 批量管理线程对象,也可以在函数之间转移所有权。工程实践中更推荐用 RAII 包装线程,在析构函数中自动选择 join 或 detach,避免异常路径下的 terminate。这虽然增加了一点封装成本,但会让生命周期管理更安全。
二、同步机制:锁、条件变量与原子操作
多个线程同时读写同一变量时,若没有同步措施,就构成数据竞争,行为未定义。最简单的保护手段是互斥量 std::mutex。它提供 lock 和 unlock 接口,通常与 std::lock_guard 或 std::unique_lock 配合使用。lock_guard 构造时加锁,析构时解锁,适合作用域固定的加锁场景。下面的计数器使用互斥量保护自增操作:
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
int counter = 0;
std::mutex mtx;
void increment(int times) {
for (int i = 0; i < times; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(increment, 100000);
}
for (auto& t : threads) {
t.join();
}
std::cout << "counter = " << counter << "\n";
return 0;
}
仅靠锁并不能解决线程间需要等待的问题。比如生产者生产数据后,消费者应阻塞直到队列非空。std::condition_variable 专门用于这种等待与通知场景。它需要与 std::unique_lock 搭配,因为锁需要更灵活的释放与重新获取。wait 的第二个参数是谓词,可以防止虚假唤醒。下面是一个简单的生产者消费者模型:
#include <condition_variable>
#include <deque>
#include <iostream>
#include <mutex>
#include <thread>
std::deque<int> queue;
std::mutex queue_mtx;
std::condition_variable cv;
void producer(int count) {
for (int i = 0; i < count; ++i) {
std::unique_lock<std::mutex> lock(queue_mtx);
queue.push_back(i);
cv.notify_one();
}
}
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(queue_mtx);
cv.wait(lock, [] { return !queue.empty(); });
int value = queue.front();
queue.pop_front();
lock.unlock();
std::cout << "consume " << value << "\n";
}
}
原子操作 std::atomic 为单一变量提供不加锁的线程安全修改。对于简单的计数器或标志位,使用 std::atomic 比互斥量更轻量,也更容易表达意图。默认的 memory_order_seq_cst 虽然语义最强,但可能需要额外同步开销。在性能敏感的场景,可以根据实际依赖关系选择更弱的内存序,不过多数业务代码先保证正确性再考虑放宽内存序更稳妥。需要避免把原子操作当作解决所有并发问题的银弹,复杂的数据结构仍需锁或其他同步原语。
三、异步任务与线程池:从 std::async 到可复用执行模型
如果只关心任务的返回值,而不想手动管理 std::thread,std::async 是更直接的方案。它返回 std::future 对象,通过 get 等待结果并获取返回值,异常也会在 get 时被重新抛出。std::async 的底层可能创建线程,也可能延迟执行,取决于启动策略。下面的例子展示了 std::async 的基本用法:
#include <future>
#include <iostream>
int compute(int x) {
return x * x + 1;
}
int main() {
std::future<int> result = std::async(std::launch::async, compute, 10);
std::cout << "result = " << result.get() << "\n";
return 0;
}
std::async 每次调用都可能创建新线程,在高频任务场景下,线程创建和销毁的开销不容忽视。线程池提前创建固定数量的工作线程,任务被推入队列后由空闲线程取走执行。一个基础线程池通常包含任务队列、互斥量、条件变量和一组 std::thread。下面是一个最简实现,能帮助理解线程池的工作流程:
#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
#include <future>
#include <utility>
class ThreadPool {
public:
explicit ThreadPool(size_t count) : stop(false) {
for (size_t i = 0; i < count; ++i) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mtx);
cv.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
template<class F>
void enqueue(F&& f) {
{
std::unique_lock<std::mutex> lock(queue_mtx);
tasks.emplace(std::forward<F>(f));
}
cv.notify_one();
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mtx);
stop = true;
}
cv.notify_all();
for (std::thread& worker : workers) {
worker.join();
}
}
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mtx;
std::condition_variable cv;
bool stop;
};
这个简单线程池演示了生产者消费者模式的实际应用。任务提交方是生产者,工作线程是消费者。析构函数中先设置 stop 标志,再通过 notify_all 唤醒所有等待线程,最后逐个 join,确保线程资源被正确回收。生产环境中还会扩展任务优先级、任务窃取、可等待的 future 返回等特性,但核心结构不变。
选择线程池线程数量时,通常与硬件并发能力相关。IO 密集型任务可以设置较多线程,CPU 密集型任务则不宜超过可用核心数太多,否则上下文切换会拖慢整体速度。无论采用什么抽象,都应该先测量实际负载下的性能,再决定线程数量与队列长度。理解同步原语的行为,比单纯记住 API 名称更重要,这也是从“能写多线程”到“会写多线程”的关键一步。
C++多线程std::thread线程同步修改时间:2026-10-01 12:18:38