一个通用的异步任务提交器

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 根据内部存储的类型(通过初始化捕获时的类型)恢复原始的值类别
相关推荐
闻道且行之1 小时前
Hair Segmentation:MediaPipe 头发分割模块 CMake 独立编译
c++·人工智能·深度学习·神经网络·opencv·计算机视觉
Irissgwe1 小时前
C++ STL 详解:list 的介绍使用与模拟实现
开发语言·c++·stl·list
我能坚持多久1 小时前
C++继承详解
开发语言·c++
Brilliantwxx2 小时前
【C++】 手撕哈希表:封装 unordered_set和unordered_map
c++·哈希算法·散列表
Rookie Linux2 小时前
使用Qt6 QML以及第三方库FluentUI、PCapPlusPlus开发一个自定义抓包软件
网络·c++·qt·cmake·qml
江屿风2 小时前
C++图论基础拓扑排序算法流食般投喂
开发语言·c++·笔记·算法·排序算法
郝学胜-神的一滴2 小时前
Qt 高级开发 030:QListWidget 右键菜单全解,从策略配置到精准删除的优雅实现
开发语言·c++·qt·程序人生·用户界面
码上有光2 小时前
map与set的使用讲解
c++·set·map·平衡二叉搜索树·关联式容器
Irissgwe2 小时前
C++ STL unordered系列关联式容器详解
开发语言·c++·stl·关联式容器