聊聊 std::packaged_task 或线程池
一、std::packaged_task:把函数包装成可调度的任务
它解决了什么问题?
std::async 很方便,但启动时机不可控(要么立即起线程,要么调用 get() 时才执行)。如果你想自己管理任务的执行时机(比如丢进线程池排队),就需要 packaged_task。
cpp
#include <future>
int compute(int x) { return x * x; }
// 把普通函数"包装"成一个任务对象
std::packaged_task<int(int)> task(compute);
// 拿到关联的 future(此时任务还没执行!)
std::future<int> fut = task.get_future();
// 任务可以被存起来、传递、稍后再执行
task(10); // 手动执行,此时才真正调用 compute(10)
std::cout << fut.get(); // → 100
关键特性:任务和”何时执行”解耦
cpp
std::packaged_task<int(int)> task(compute); std::future<int> fut = task.get_future(); // 任务可以被 move 到另一个线程去执行 std::thread t(std::move(task), 10); t.join(); std::cout << fut.get(); // → 100
本质:
packaged_task= 一个可调用对象 + 自动管理的promise。调用它时,返回值自动存进promise,再通过future取出。
为什么不直接用 std::async?
cpp
// std::async:立即决定执行方式,你无法"存起来稍后跑"
auto fut = std::async(std::launch::async, compute, 10);
// packaged_task:你完全控制"何时""在哪"执行,可以放进队列
std::queue<std::packaged_task<void()>> taskQueue;
taskQueue.push(std::packaged_task<void()>([]{ /* ... */ }));
// 线程池的 worker 线程从队列里取出来执行
这正是线程池的核心构建块——把任务”打包”放进队列,工作线程负责取出并执行。
二、线程池:为什么需要它?
问题:频繁创建/销毁线程,开销很大
cpp
// ❌ 每个任务都开一个新线程,10000个任务 = 10000次系统调用开销
for (int i = 0; i < 10000; i++) {
std::thread t([i] { compute(i); });
t.detach(); // 极度低效,且难以控制并发数量
}
线程创建涉及系统调用、栈分配等,开销远大于一次函数调用。线程池的思路:提前创建固定数量的线程,反复复用,任务排队等待空闲线程处理。
线程池架构图
任务提交 任务队列 工作线程池
│ │ │
submit(task1) ──┐ │ ┌─► Thread1 (循环取任务执行)
submit(task2) ──┼──► [task1, task2, task3] ───┼─► Thread2 (循环取任务执行)
submit(task3) ──┘ │ └─► Thread3 (循环取任务执行)
│
(线程数量固定,
不随任务数增长)
简化版线程池实现
cpp
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
class ThreadPool {
public:
ThreadPool(size_t numThreads) : stop(false) {
for (size_t i = 0; i < numThreads; ++i) {
workers.emplace_back([this] { workerLoop(); });
}
}
// 提交任意可调用对象,返回 future 获取结果
template<typename F, typename... Args>
auto submit(F&& f, Args&&... args)
-> std::future<std::invoke_result_t<F, Args...>>
{
using ReturnType = std::invoke_result_t<F, Args...>;
// 用 packaged_task 包装任务(绑定参数)
auto task = std::make_shared<std::packaged_task<ReturnType()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<ReturnType> result = task->get_future();
{
std::lock_guard<std::mutex> lock(queueMutex);
tasks.emplace([task] { (*task)(); }); // 包装成 void() 放入队列
}
condition.notify_one(); // 唤醒一个等待中的工作线程
return result;
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(queueMutex);
stop = true;
}
condition.notify_all();
for (auto& worker : workers) worker.join();
}
private:
void workerLoop() {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queueMutex);
// 等待:有任务 或 线程池要关闭
condition.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 queueMutex;
std::condition_variable condition;
bool stop;
};
使用示例
cpp
int main() {
ThreadPool pool(4); // 4个工作线程
std::vector<std::future<int>> results;
// 提交100个任务,但只用4个线程处理
for (int i = 0; i < 100; ++i) {
results.push_back(pool.submit([i] {
return i * i;
}));
}
// 取出所有结果
for (auto& fut : results) {
std::cout << fut.get() << " ";
}
// 输出 0 1 4 9 16 25 ... 9801(任务在4个线程间自动调度完成)
}
关键设计点逐个拆解
1️⃣ 为什么用 condition_variable?
cpp
condition.wait(lock, [this] { return stop || !tasks.empty(); });
没有任务时,工作线程休眠而不是空转检查(避免浪费 CPU)。有新任务 notify_one() 时才被唤醒。
2️⃣ 为什么用 std::function<void()> 存任务?
不同任务的返回类型、参数都不同(int(int)、void()、string(double)…)。用 packaged_task 包装后,统一抹平成 void()——类型擦除,方便统一存进队列。
3️⃣ 为什么 submit 要返回 future?
调用方需要知道任务何时完成、结果是什么——这正是前面讲的 future/promise 机制的复用。
三者关系串联起来看
std::thread ──► 最底层:手动管理单个线程,无返回值
│
std::async ──► 自动开线程 + 返回 future,但不可控制调度
│
std::packaged_task ──► 任务和执行解耦,可以存储、传递、延迟执行
│
线程池 ──► packaged_task + 队列 + 固定线程数,
实现"任务排队、线程复用"的工程级方案
一句话总结
packaged_task是”打包好的、待执行的函数+future”;线程池则是用任务队列 + 固定线程数 + 条件变量,把大量”打包任务”高效地分发给少量线程执行,避免频繁创建销毁线程的开销。这是几乎所有高并发 C++ 服务端框架的基础构件。
想继续看 C++20 的 std::jthread(自动 join 的线程)或者更进阶的无锁队列优化线程池性能吗?
