多线程编程进阶
需要预先学习:
1. 线程间通信的问题
锁解决了"互斥"(同一时刻只有一个线程进临界区),但没解决"等待":线程 A 要等线程 B 完成某件事(比如数据准备好了)才能继续,A 应该怎么等?
- 忙等:
while (!ready) {}死循环空转,浪费 CPU,还带来数据竞争 - 轮询:
while (!ready) { sleep(1ms); }浪费少些,但有延迟,且ready的读写仍要同步
这两种方案都不优雅。标准答案有两个:条件变量(等待"事件")和 future/promise(等待"结果")。本篇文章围绕它们展开,最后实现一个线程池。
2. 条件变量 std::condition_variable
条件变量(condition variable)让线程阻塞等待某个条件成立,由其他线程唤醒它。它必须配合 std::unique_lock(和一把互斥量)使用。
核心 API:
| 函数 | 行为 |
|---|---|
wait(lock) |
阻塞,并自动解锁;被唤醒后重新加锁 |
wait(lock, pred) |
带谓词版本:谓词为 false 才阻塞(见下文) |
notify_one() |
唤醒一个正在等待的线程 |
notify_all() |
唤醒所有正在等待的线程 |
先看标准的使用模式——生产者消费者:
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue; // 受 mtx 保护的共享队列
bool done = false;
void producer()
{
for (int i = 0; i < 5; ++i)
{
{
std::lock_guard<std::mutex> lock(mtx);
data_queue.push(i);
std::cout << "生产: " << i << std::endl;
}
cv.notify_one(); // 唤醒一个消费者
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
{
std::lock_guard<std::mutex> lock(mtx);
done = true;
}
cv.notify_all(); // 通知所有消费者收工
}
void consumer()
{
while (true)
{
std::unique_lock<std::mutex> lock(mtx);
// 等待条件: 队列非空 或 生产结束
cv.wait(lock, [&] { return !data_queue.empty() || done; });
if (!data_queue.empty())
{
int value = data_queue.front();
data_queue.pop();
lock.unlock(); // 处理数据前先解锁, 让其他线程也能进临界区
std::cout << "消费: " << value << std::endl;
}
else if (done)
{
break; // 队列空且生产结束, 退出
}
}
}
int main()
{
std::thread p(producer);
std::thread c1(consumer);
std::thread c2(consumer);
p.join();
c1.join();
c2.join();
return 0;
}
wait 的关键语义:调用 wait 时,条件变量原子地完成"解锁 + 睡眠"两步——线程带着锁进入睡眠,锁被释放,其他线程才能进入临界区修改条件;被唤醒后,wait 返回前重新加锁。这个"解锁与睡眠不可分割"的设计避免了竞态:否则可能出现"线程正要睡眠,生产者已经 notify,通知丢失,消费者永远睡下去"。
2.1 为什么用谓词重载的 wait
直接 wait(lock) 有两个问题:
1. 虚假唤醒(spurious wakeup):wait 可能在没有任何线程 notify 的情况下返回(这是操作系统实现的现实,标准明确允许)。如果唤醒后不检查条件直接消费,会读到空队列。
2. 多消费者竞争:notify_all 唤醒所有消费者,但只有一个能抢到锁。抢到的消费完队列后,另一个被唤醒的线程醒来发现队列空了——它必须再检查一次条件。
wait(lock, pred) 等价于 while (!pred()) wait(lock),循环检查条件,从根本上解决了上面两个问题:
cv.wait(lock, [&] { return !data_queue.empty(); });
// 等价于:
while (!(!data_queue.empty()))
{
cv.wait(lock);
}
注:谓词版本内部就是"检查谓词 → 不满足才 wait"的循环,所以永远应该用谓词重载,裸
wait(lock)只在极少数明确知道不会被虚假唤醒干扰的场景才用。
2.2 notify_one 与 notify_all
notify_one():唤醒一个等待线程。适合"一个事件只需一个线程处理"的场景(如队列来了一个任务)。唤醒哪个线程由调度器决定,不确定。notify_all():唤醒所有等待线程。适合"广播"场景(如done = true让所有消费者退出)。
选择原则:能确定只有一个消费者关心时用 notify_one(开销小、竞争少);需要所有线程都醒来检查时用 notify_all。
注意:notify 不要求持锁——先释放锁再 notify 是更好的实践(如上例生产者)。如果持锁 notify,被唤醒的线程立即尝试加锁却拿不到,白白多一次阻塞-唤醒往返。
2.3 condition_variable_any
std::condition_variable_any 可以与任何满足 BasicLockable 要求的锁配合(包括 std::shared_lock、自定义锁),而普通 condition_variable 只能用 std::unique_lock<std::mutex>。代价是性能略低(内部多做一层抽象),一般用不上。
3. 异步任务:async、future、promise
条件变量适合"等待事件",而"等待一个计算结果"用 future/promise 模型更直接。三个组件:
std::async:启动一个异步任务,返回std::futurestd::future:代表"未来的结果",用get()阻塞等待结果std::promise:手动设置结果,通过get_future()把结果传给future
3.1 std::async:最简单的异步调用
#include <future>
#include <iostream>
int slow_square(int n)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return n * n;
}
int main()
{
// 启动异步任务, 立即返回 future, 不阻塞
std::future<int> fut = std::async(std::launch::async, slow_square, 12);
std::cout << "异步任务已启动, 主线程可以干别的事" << std::endl;
int result = fut.get(); // 阻塞直到结果就绪
std::cout << "结果 = " << result << std::endl; // 144
return 0;
}
std::async 的三个要点:
- 返回值是
std::future<T>,T是任务函数的返回类型 fut.get()只能调用一次,第二次调用抛std::future_error;wait()只等待不取结果- 任务函数抛出的异常会被捕获并重新抛出在
get()处——异步任务的错误处理和同步调用一样自然
3.2 启动策略 launch::async 与 launch::deferred
| 策略 | 行为 |
|---|---|
std::launch::async |
立即在新线程中执行 |
std::launch::deferred |
懒执行:直到 get()/wait() 才在当前线程执行 |
std::launch::async | deferred |
实现自行选择(默认) |
auto f1 = std::async(std::launch::async, task); // 必定新线程, 立即执行
auto f2 = std::async(std::launch::deferred, task); // 惰性, get() 时当前线程执行
auto f3 = std::async(task); // 实现决定, 通常是 async
陷阱:默认策略下,如果任务可能不被 get(),它可能根本不会执行(deferred)。而 std::async 返回的 future 析构时会阻塞等待任务完成(当任务是 launch::async 时)——fut.get() 没调用但 future 析构了,程序会等到任务结束。这是标准规定的行为,初看反直觉:
void task() { std::this_thread::sleep_for(std::chrono::seconds(1)); }
int main()
{
std::async(std::launch::async, task); // 临时 future 析构时阻塞 1 秒!
std::cout << "这句话 1 秒后才打印" << std::endl;
return 0;
}
所以始终把 async 的返回值存下来,并且明确选择策略,不要依赖默认行为。
3.3 std::promise:手动设置结果
std::promise<T> 是 future 的"写入端":创建 promise 后用 get_future() 得到配套的 future,把 promise 传进线程(用 std::move,见《线程传参》),线程算完后 set_value() 或 set_exception():
#include <future>
#include <iostream>
#include <thread>
void compute(std::promise<int> prom, int n)
{
try
{
if (n < 0)
{
throw std::invalid_argument("负数没有平方根");
}
prom.set_value(n * n); // 设置结果
}
catch (...)
{
prom.set_exception(std::current_exception()); // 或设置异常
}
}
int main()
{
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(compute, std::move(prom), 7); // promise 不可拷贝, 移动进线程
t.join();
try
{
std::cout << "结果 = " << fut.get() << std::endl; // 49
}
catch (const std::exception& e)
{
std::cout << "异常: " << e.what() << std::endl;
}
return 0;
}
关键语义:
- promise 是 move-only 类型,必须
std::move进线程 set_value后 promise 进入就绪态,future 的get()不再阻塞- promise 析构时若尚未 set_value,future 会收到
std::future_error(broken promise)——注意这个语义,它提示你"承诺的结果永远不会来了" future可以wait_for(duration)/wait_until(time_point)限时等待,超时返回std::future_status::timeout
if (fut.wait_for(std::chrono::milliseconds(100)) == std::future_status::timeout)
{
std::cout << "100ms 内没算完, 先做别的事" << std::endl;
}
3.4 std::packaged_task:打包任务
std::packaged_task 把可调用对象和 future 打包:调用 packaged_task 的 operator() 时执行任务并把结果写入内部的 promise。适合"任务本身要排队/延迟执行,但调用方想拿结果"的场景:
#include <future>
#include <iostream>
#include <thread>
int add(int a, int b) { return a + b; }
int main()
{
std::packaged_task<int(int, int)> task(add); // 打包任务
std::future<int> fut = task.get_future(); // 先拿到结果的 future
std::thread t(std::move(task), 3, 4); // 在线程中执行任务
t.join();
std::cout << "3 + 4 = " << fut.get() << std::endl; // 7
return 0;
}
三者的分工可以这样理解:async 是"开箱即用"(启动 + 拿结果一步到位),promise 是"手动喂结果"(结果来自任何地方,不一定是函数返回值),packaged_task 是"把任务延迟到任意线程执行"(线程池的常用积木)。
3.5 std::shared_future:多线程共享结果
std::future 的 get() 是独占的——只能调用一次。如果多个线程都要读同一个结果,用 std::shared_future:调用 fut.share() 从 future 转换得到,之后每个线程都可以 get()(内部共享同一份结果):
#include <future>
#include <iostream>
#include <thread>
#include <vector>
int main()
{
std::future<int> fut = std::async(std::launch::async, [] {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return 42;
});
std::shared_future<int> shared = fut.share(); // 转成共享 future
std::vector<std::thread> workers;
for (int i = 0; i < 3; ++i)
{
workers.emplace_back([shared, i] { // shared_future 可拷贝
std::cout << "线程 " << i << " 读到结果: " << shared.get() << std::endl;
});
}
for (auto& t : workers)
{
t.join();
}
return 0;
}
4. 线程池
线程池(thread pool)预先创建一批线程,反复复用它们执行任务队列中的任务,避免"每个任务都创建/销毁线程"的开销。核心组件:
- 任务队列(受锁保护)+ 条件变量
- 固定数量的工作线程:循环从队列取任务执行
- 提交接口:把任务包装成
packaged_task放入队列,返回 future
实现一个通用的线程池:
#include <condition_variable>
#include <functional>
#include <future>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
class ThreadPool
{
public:
explicit ThreadPool(size_t thread_count)
{
for (size_t i = 0; i < thread_count; ++i)
{
workers_.emplace_back([this] { worker_loop(); });
}
}
// 提交任务, 返回 future 用于获取结果
template <typename F, typename... Args>
auto submit(F&& f, Args&&... args)
-> std::future<std::invoke_result_t<F, Args...>>
{
using ResultType = std::invoke_result_t<F, Args...>;
// 把任务打包成 packaged_task, 结果写入 future
auto task = std::make_shared<std::packaged_task<ResultType()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...));
std::future<ResultType> result = task->get_future();
{
std::lock_guard<std::mutex> lock(queue_mtx_);
tasks_.push([task] { (*task)(); }); // 任务队列中存放 lambda
}
cv_.notify_one(); // 唤醒一个工作线程
return result;
}
~ThreadPool()
{
{
std::lock_guard<std::mutex> lock(queue_mtx_);
stop_ = true;
}
cv_.notify_all(); // 唤醒所有线程检查停止标志
for (auto& worker : workers_)
{
worker.join();
}
}
private:
void worker_loop()
{
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(); // 队列外执行任务
}
}
std::vector<std::thread> workers_;
std::queue<std::function<void()>> tasks_;
std::mutex queue_mtx_;
std::condition_variable cv_;
bool stop_ = false;
};
使用:
int main()
{
ThreadPool pool(4); // 4 个工作线程
// 提交 8 个任务, 每个任务返回自己的编号的平方
std::vector<std::future<int>> futures;
for (int i = 0; i < 8; ++i)
{
futures.push_back(pool.submit([i] {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
return i * i;
}));
}
int sum = 0;
for (auto& fut : futures)
{
sum += fut.get(); // 逐个取结果
}
std::cout << "sum = " << sum << std::endl; // 0+1+4+9+16+25+36+49 = 140
return 0;
}
这个实现的关键点:
- 任务类型擦除:队列里存
std::function<void()>,任意签名任务经 lambda 包装后统一入队 - 结果传递:
packaged_task的 future 在submit时就返回,任务真正执行时结果才就绪——get()自然阻塞等待 - 停止协议:析构时置
stop_ = true并notify_all,工作线程清空剩余任务后退出 - 任务在锁外执行:执行任务时不持锁,长任务不会阻塞其他线程取任务
注:这只是教学版线程池。生产级线程池还需要考虑:任务异常隔离、动态扩缩容、任务优先级、
std::jthread(C++20,支持协作式取消)等。
5. 线程优先级
C++ 标准库没有线程优先级接口——优先级是操作系统概念,需要通过平台 API 操作。std::thread::native_handle() 返回线程的原生句柄,可以传给系统 API:
#include <iostream>
#include <thread>
int main()
{
std::thread t([] { /* 工作 */ });
// Linux: pthread API (需要 -pthread)
pthread_t handle = t.native_handle();
struct sched_param param;
param.sched_priority = 10;
pthread_setschedparam(handle, SCHED_OTHER, ¶m);
// Windows: Win32 API
// HANDLE h = t.native_handle();
// SetThreadPriority(h, THREAD_PRIORITY_ABOVE_NORMAL);
t.join();
return 0;
}
关于优先级要注意:
- 提高优先级不等于更快:优先级只影响"竞争 CPU 时的调度顺序",单核满载时高优先级线程先获得 CPU
- 实时调度策略(
SCHED_FIFO/SCHED_RR)需要 root 权限,且可能饿死其他线程,服务器上慎用 - 多数应用不需要手动调优先级:让操作系统按默认策略调度,把精力放在减少持锁时间和合理分片上
- 优先级与锁的交互有陷阱:优先级反转(priority inversion)——低优先级线程持锁,高优先级线程等锁,中优先级线程抢占低优先级线程,高优先级被间接饿死。Linux 的
pthread_mutex默认不做优先级继承,需要时用PTHREAD_PRIO_INHERIT属性
6. 线程局部存储 thread_local
thread_local 声明的变量每个线程有一份独立副本,线程之间互不可见、互不干扰。它解决了"全局状态在多线程下不可用"的问题:
#include <iostream>
#include <thread>
thread_local int counter = 0; // 每个线程独立的 counter
void work(const char* name)
{
for (int i = 0; i < 3; ++i)
{
++counter; // 只改自己线程的那一份
}
std::cout << name << " 的 counter = " << counter << std::endl;
}
int main()
{
std::thread t1(work, "线程1");
std::thread t2(work, "线程2");
t1.join();
t2.join();
std::cout << "主线程的 counter = " << counter << std::endl;
return 0;
}
线程1 的 counter = 3
线程2 的 counter = 3
主线程的 counter = 0
三个线程各自维护自己的 counter,不需要任何锁——这是 thread_local 最大的价值:把"共享"变成"每线程私有",从根源上消除竞争。
使用场景:
- 每线程缓存:如随机数引擎(
thread_local std::mt19937)、内存分配器的线程缓存 - 每线程上下文:错误码、日志前缀、数据库连接
- 懒初始化单例的每线程版本
注意事项:
thread_local变量在线程首次使用时初始化(而不是线程创建时)- 生命周期:线程结束时析构。detach 的线程结束后其
thread_local变量也销毁,不要跨线程持有指向他人thread_local变量的指针 thread_local与static可以组合(static thread_local),语义仍是每线程一份- 每个线程的
thread_local有独立存储,线程数量多时内存占用按线程数翻倍
典型应用——线程安全的一次性初始化(每线程各做各的):
thread_local std::string thread_name;
void ensure_name(const std::string& name)
{
if (thread_name.empty())
{
thread_name = name; // 只影响当前线程
}
}
7. 设施对比总结
| 设施 | 解决的问题 | 关键 API |
|---|---|---|
condition_variable |
等待事件 / 条件 | wait、notify_one、notify_all |
std::async |
一步启动异步任务并取结果 | std::launch 策略、future::get |
std::promise |
手动设置异步结果 | set_value、set_exception、get_future |
std::packaged_task |
把任务与结果打包、延迟执行 | get_future、operator() |
std::shared_future |
多个线程共享同一结果 | share()、多次 get() |
| 线程池 | 复用线程,避免反复创建销毁 | submit 返回 future |
thread_local |
每线程独立存储,免锁 | thread_local 关键字 |
选择建议:等一个结果用 async/future;等一个条件/事件用条件变量;任务量大、频繁提交用线程池;每线程独立状态用 thread_local。裸 std::thread 留给"需要完全控制线程生命周期"的场景。
到这一步,多线程的同步与通信手段基本齐全了。还剩下最后一个主题:原子操作——不用锁也能保证正确性的轻量同步手段,以及它背后的硬件原理。
需要进一步学习:


