i007.cc

i007.cc

优先队列-降维打击

05.价值资料

聊聊 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 的线程)或者更进阶的无锁队列优化线程池性能吗?

发表回复