1、背景
多线程编程中,我们经常要把一个函数扔到线程池里执行。最简单的情况:
cpp
void hello() { std::cout << "Hello\n"; }
thread_pool.post(hello); // 只执行,不关心返回值
但很快你就会碰到两个需求,1)函数需要参数:比如 print("hello", 42);2)需要获取返回值:比如 int result = add(3,5); 但要在另一个线程执行。用原生 std::thread 很难优雅地解决这两个问题,而 std::packaged_task + std::future + 万能引用的组合可以完美解决。
2、std::packaged_task任务包装器
std::packaged_task 是一个模板类,它包装一个可调用对象,让你可以异步获取结果。
cpp
#include <future>
#include <iostream>
int add(int a, int b) { return a + b; }
int main() {
// 包装一个返回 int,接受两个 int 的函数
std::packaged_task<int(int, int)> task(add);
// 获取与 task 关联的 future
std::future<int> result = task.get_future();
// 在另一个线程执行 task, 这里的move不能删除,否则编译不过
std::thread t(std::move(task), 3, 5);
// 主线程等待结果
std::cout << result.get() << std::endl; // 输出 8
t.join();
}
需要注意的是:
- std::packaged_task 的模板参数是函数签名:<返回值(参数类型...)>
- get_future() 返回一个 std::future,用于获取结果
- packaged_task 是只移动类型(不可拷贝),因为里面对应一个异步状态
3、std::bind 绑定参数 ------ 把多参数变成零参数
bind只收拢⼊参、绝对不会修改原函数返回值。它的核⼼作⽤:将N参数可调⽤对象=>0参数可调⽤对象,函数内部执⾏逻辑、返回值类型、返回值内容完全和原函数⼀致,⽆任何改动。线程池的任务队列通常只存 std::function<void()>(无参、无返回值的任务)。如果你的原始函数有参数,需要先绑定参数,变成一个无参函数对象。
cpp
#include <functional>
void print(const std::string& s, int n) {
std::cout << s << n << std::endl;
}
// 绑定参数,生成一个无参函数对象
auto bound = std::bind(print, "value: ", 42);
bound(); // 等价于 print("value: ", 42)
如果我们想异步执行 print 并把结果(void)也包装成任务,就需要与packaged_task 结合:
cpp
// 注意 std::packaged_task<void()> 表示无参无返回值任务
std::packaged_task<void()> task(std::bind(print, "value: ", 42));
auto fut = task.get_future();
std::thread t(std::move(task));
fut.wait(); // 等待任务完成
t.join();
4、万能引用+完美转发
你现在可能想写一个通用函数 post(F&& f, Args&&... args),能接受任意函数和参数,自动创建 packaged_task 并提交到线程池。这就要用到万能引用和完美转发。什么事万能引用呢?一个函数模板的参数如果写成 T&&,并且 T 是模板参数,那么它就是万能引用(universal reference)。它可以绑定到左值或右值。
cpp
template<typename T>
void foo(T&& arg) { // arg 是万能引用
// ...
}
// 传入左值,T 推导为左值引用类型,arg 类型为左值引用
// 传入右值,T 推导为非引用类型,arg 类型为右值引用
当你拿到万能引用参数后,想把它原封不动地传递给另一个函数,就用 std::forward 来保持它的值类别(左值/右值)
cpp
template<typename T>
void wrapper(T&& arg) {
// 完美转发 arg 给 target
target(std::forward<T>(arg));
}
template<class F, class... Args>
void post(F&& f, Args&&... args) {
// 使用 std::bind 把函数和参数绑定成无参函数
auto bound = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
// 创建 packaged_task<void()> 包装这个无参函数
std::packaged_task<void()> task(std::move(bound));
// 获取 future(虽然这里没使用,但任务执行时需要)
auto future = task.get_future();
// 启动一个线程执行任务
std::thread t(std::move(task));
t.detach(); // 简单示例:分离线程
}
// 测试
void sayHello(std::string name) {
std::cout << "Hello " << name << std::endl;
}
post(sayHello, "World"); // 输出 Hello World
5、返回 std::future ------ 让调用者获取结果
上面的 post 不能获取返回值。我们需要另一个版本 postAndReturn,它返回 std::future
cpp
template<class F, class... Args>
auto postAndReturn(F&& f, Args&&... args)
-> std::future<typename std::result_of<F(Args...)>::type>
{
using return_type = typename std::result_of<F(Args...)>::type;
// 绑定函数和参数
auto bound = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
// 注意这里模板参数是 return_type()
// 因为 task 是局部变量,离开函数作用域会被销毁。我们用 shared_ptr 包裹它,再让线程里的 lambda 捕获这个 shared_ptr,保证任务执行时 task 还活着
auto task = std::make_shared<std::packaged_task<return_type()>>(std::move(bound));
std::future<return_type> result = task->get_future();
// 启动线程,捕获 task 的 shared_ptr 保证生命周期
std::thread([task]() { (*task)(); }).detach();
return result;
}
// 测试
int add(int a, int b) { return a + b; }
auto fut = postAndReturn(add, 3, 5);
std::cout << fut.get() << std::endl; // 8
6、用 lambda 替代 std::bind
从 C++14 开始,你可以用 lambda 捕获参数包,完全取代 std::bind,代码更简洁、更安全。
cpp
template<class F, class... Args>
auto postAndReturnModern(F&& f, Args&&... args)
-> std::future<std::invoke_result_t<F, Args...>>
{
// std::invoke_result_t:C++17 中替代 std::result_of
using return_type = std::invoke_result_t<F, Args...>;
auto task = std::make_shared<std::packaged_task<return_type()>>(
// 广义 lambda 捕获 + 包展开。把每个参数按完美转发的方式捕获到 lambda 内部
// mutable:因为捕获的 f 和 args 可能是右值,调用它们时可能需要修改(比如移动),mutable 允许 lambda 修改捕获的副本
[f = std::forward<F>(f), ...args = std::forward<Args>(args)]() mutable {
// std::invoke:统一调用,比直接 f(args...) 更通用(支持成员指针等)
return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}
);
std::future<return_type> result = task->get_future();
std::thread([task]() { (*task)(); }).detach();
return result;
}
7、注意事项
- 不要捕获引用悬空,如果传入的参数是左值引用,并且该引用指向的对象生命周期比任务短,就会悬空。确保任务执行时,所有参数都有效(通常按值捕获或使用 std::shared_ptr 管理)
- packaged_task 只能执行一次,调用 operator() 后,任务的状态变为已使用,不能再调用。因此不要多次 (*task)()
- 异常传递,如果 f 抛出异常,该异常会被存储在 future 中,在 future.get() 时重新抛出
cpp
auto fut = postAndReturn( []() { throw std::runtime_error("oops"); } );
try {
fut.get();
} catch(const std::runtime_error& e) {
std::cout << e.what() << std::endl; // "oops"
}
- 完美转发需要两次,在 lambda 内部调用 std::invoke 时,依然需要对捕获的 f 和 args 再次使用 std::forward,因为捕获后的变量是左值(即使原来捕获的是右值引用)。std::forward 根据内部存储的类型(通过初始化捕获时的类型)恢复原始的值类别