C++扩展 --- 并发支持库(补充3)

C++扩展 --- 并发支持库(补充2)https://blog.csdn.net/Small_entreprene/article/details/149854780?spm=1001.2014.3001.5501

C++ 异步编程:用 future 库优雅获取线程返回值

在多线程编程中,我们经常需要从线程中获取任务执行的结果。比如启动一个线程压缩文件后,我们需要知道压缩后的文件名和大小。在 C++11 之前,获取线程返回值并不直观,通常需要借助指针、互斥锁和条件变量的组合,代码复杂且容易出错。

传统方式的痛点

先看看没有 future 库时,我们是如何获取线程返回值的:

cpp 复制代码
#include<iostream>
#include<thread>
#include<mutex>

void fun(int x, int y, int* ans) {
    *ans = x + y;
}

int main()
{
    int a = 10;
    int b = 8;

    int* sum = new int(0);
    std::thread t(fun, a, b, sum);
    t.join();

    // 获取线程的"返回值"
    std::cout << *sum << std::endl; // 输出:18
    delete sum;
    
    return 0;
}

这段代码虽然能工作,但存在明显问题:

  • 需要手动管理动态内存(new/delete)
  • 必须显式调用 join () 等待线程完成
  • 无法优雅地处理多个返回值
  • 没有异常处理机制

如果线程需要在不同阶段返回多个结果,代码会变得更加复杂,需要更多的同步机制来协调线程间的数据传递。

什么是 future 库?

C++11 引入的<future>头文件彻底改变了这种状况,它提供了一套完整的异步编程机制,让我们能以更简洁、安全的方式处理异步操作和获取结果。

简单来说,std::future就像是一张 "提货单":当你启动一个异步任务时,它会立即给你返回一个 future 对象,你可以在需要结果的时候,用这张 "提货单" 来获取任务的结果。如果任务还没完成,它会耐心等待;如果任务已经完成,就直接返回结果。

future 的核心组件

<future>库主要包含以下几个核心组件:

  1. std::future:获取异步操作结果的 "提货单"
  2. std::async:启动异步任务的便捷函数
  3. std::promise:用于主动设置异步操作结果
  4. std::packaged_task:封装可调用对象为异步任务

下面我们逐一了解这些组件的用法。

1. std::async:最简单的异步任务

std::async是启动异步任务最简便的方式,它会自动创建线程并运行任务,返回一个std::future对象供我们获取结果。

cpp 复制代码
#include <iostream>
#include <future>
#include <thread>
#include <chrono>

// 一个简单的任务函数,计算两数之和
int add(int a, int b) {
    // 模拟耗时操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return a + b;
}

int main() {
    std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
    
    // 启动异步任务,立即返回future对象
    std::future<int> result = std::async(add, 10, 20);
    
    // 主线程可以继续做其他事情
    std::cout << "等待结果中..." << std::endl;
    
    // 获取结果,如果任务没完成会阻塞等待
    std::cout << "10 + 20 = " << result.get() << std::endl;
    
    return 0;
}

这段代码比传统方式简洁得多,我们不需要手动创建线程、管理内存,也不需要显式调用 join ()。

启动策略

std::async提供了两种启动策略:

  • std::launch::async:立即创建新线程执行任务
  • std::launch::deferred:延迟执行,直到调用 get () 时才在当前线程执行
cpp 复制代码
#include <iostream>
#include <future>
#include <thread>
#include <chrono>

void task(const char* name) {
    std::cout << name << " 开始执行,线程ID: " 
              << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << name << " 完成" << std::endl;
}

int main() {
    std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
    
    // 异步执行(新线程)
    auto f1 = std::async(std::launch::async, task, "异步任务");
    
    // 延迟执行(调用get时执行)
    auto f2 = std::async(std::launch::deferred, task, "延迟任务");
    
    std::cout << "主线程工作..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    
    f1.get();  // 等待异步任务完成
    f2.get();  // 此时才执行延迟任务
    
    return 0;
}

2. std::promise:主动设置结果

在 C++11 引入的并发编程模型中,std::promisestd::future是一对相辅相成的工具,它们共同构建了异步操作中 "结果传递" 的桥梁。简单来说,std::promise负责 "生产" 结果,而std::future负责 "消费" 结果,二者通过一个隐藏的 "共享状态" 实现跨线程通信。

先看一个基础示例,感受它们的协作方式:

cpp 复制代码
#include <iostream>
#include <future>
#include <thread>
#include <chrono>

int main() {
    // 创建promise与对应的future,二者绑定到同一共享状态
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();

    // 启动线程执行异步任务,通过引用捕获promise
    std::thread t([&prom]() {
        // 模拟耗时操作(如网络请求、文件IO等)
        std::this_thread::sleep_for(std::chrono::seconds(1));
        // 任务完成后,通过promise设置结果,触发共享状态就绪
        prom.set_value(42);  
    });

    // 主线程中,future等待结果就绪并获取
    std::cout << "等待异步结果..." << std::endl;
    // get()会阻塞直到结果可用,且只能调用一次
    std::cout << "获取到的结果: " << fut.get() << std::endl;

    t.join();
    return 0;
}

std::promise的独特价值

std::promise最强大的特性在于其 "主动性"------ 它允许我们在任意时机、任意线程中设置结果,而非局限于异步任务本身。这种灵活性使其适用于多种场景:

  • 分阶段任务 :例如一个线程负责预处理数据,另一个线程在预处理完成后通过promise设置中间结果,主线程则通过future获取并继续处理。
  • 异常传递 :当异步操作发生错误时,可通过promise.set_exception()将异常存储到共享状态,future.get()会在主线程中重新抛出该异常,实现跨线程异常安全传递。
  • 外部事件触发 :比如等待用户输入、信号量触发等外部事件,完成后通过promise手动标记结果就绪。

std::future的核心区别

虽然二者总是成对出现,但职责边界清晰:

维度 std::promise std::future
角色 结果的 "生产者" 结果的 "消费者"
核心操作 set_value()/set_exception()(设置结果) get()/wait()(获取 / 等待结果)
状态影响 主动将共享状态置为 "就绪" 被动等待共享状态变为 "就绪"
生命周期 通常与生产者线程绑定 通常与消费者线程绑定
复制性 不可复制,仅可移动(确保结果唯一设置) 不可复制,仅可移动(结果只能被获取一次)

需要注意的是,future.get()是一次性操作 ------ 一旦调用,共享状态的结果会被 "取走",再次调用将导致未定义行为。如果需要在多个线程中获取同一结果,可使用std::shared_future(通过future.share()转换),它支持多次获取结果。

扩展:与其他异步工具的配合

std::promisestd::future是 C++ 异步编程的基础,它们还能与更高层次的工具配合使用:

  • std::packaged_task :将函数或可调用对象包装为一个 "任务",自动创建关联的future,本质是对promise的封装(任务执行完毕后自动调用set_value)。
  • std::async :更简洁的异步接口,可直接启动异步任务并返回future,无需手动管理线程和promise,底层可能使用线程池优化性能。

例如,用std::async简化上述示例:

cpp 复制代码
// 自动管理线程,返回的future直接关联任务结果
std::future<int> fut = std::async(std::launch::async, []() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 42;
});
std::cout << "结果: " << fut.get() << std::endl;

综上,std::promisestd::future构成了 C++ 异步编程的 "结果通道",前者赋予我们主动控制结果的能力,后者则提供了安全获取结果的机制。理解这对工具的协作模式,是掌握 C++ 并发编程的重要一步。

3. std::packaged_task:封装任务

std::packaged_task 是 C++11 引入的异步编程工具,它能将可调用对象(函数、lambda、函数对象等)包装起来,自动创建关联的 std::future,当任务执行完毕后,结果会自动存储到共享状态中,供 future 获取。

基本语法结构

cpp 复制代码
// 声明:包装返回类型为T,参数类型为Args...的可调用对象
std::packaged_task<T(Args...)> task(可调用对象);

// 获取关联的future
std::future<T> fut = task.get_future();

// 执行任务(两种方式)
task(args...);                  // 直接在当前线程执行
std::thread t(std::move(task), args...);  // 转移到新线程执行
cpp 复制代码
#include <iostream>
#include <future>
#include <cmath>
#include <thread>

// 计算平方根的函数
double compute_square_root(double x) {
    return std::sqrt(x);
}

int main() {
    // 封装任务
    std::packaged_task<double(double)> task(compute_square_root);
    // 获取future对象
    std::future<double> result = task.get_future();
    
    // 在新线程中执行任务
    std::thread th(std::move(task), 25.0);
    
    // 获取结果
    std::cout << "25的平方根是: " << result.get() << std::endl;
    
    th.join();
    return 0;
}

std::packaged_task非常适合那些需要重复执行的任务,我们可以像传递普通函数一样传递它。

future 的常用方法

std::future提供了一系列方法来管理和获取异步操作的结果:

  • get():获取结果,如果任务未完成则阻塞等待
  • wait():等待任务完成,但不获取结果
  • wait_for(duration):等待指定时长,返回等待状态
  • wait_until(timepoint):等待到指定时间点,返回等待状态
  • valid():检查 future 是否有效(是否关联到一个异步任务)
cpp 复制代码
#include <iostream>
#include <future>
#include <chrono>

int long_task() {
    std::this_thread::sleep_for(std::chrono::seconds(3));
    return 42;
}

int main() {
    auto fut = std::async(long_task);
    
    // 等待最多1秒
    auto status = fut.wait_for(std::chrono::seconds(1));
    
    if (status == std::future_status::ready) {
        std::cout << "任务已完成,结果: " << fut.get() << std::endl;
    } else if (status == std::future_status::timeout) {
        std::cout << "等待超时,任务仍在执行..." << std::endl;
    } else if (status == std::future_status::deferred) {
        std::cout << "任务被延迟执行" << std::endl;
    }
    
    // 最终还是要获取结果
    std::cout << "最终结果: " << fut.get() << std::endl;
    
    return 0;
}

异常处理

异步任务中抛出的异常会被 future 捕获,当调用get()时会重新抛出,让我们可以在主线程中统一处理异常:

cpp 复制代码
#include <iostream>
#include <future>
#include <stdexcept>

void risky_operation() {
    // 模拟一个可能失败的操作
    throw std::runtime_error("操作失败: 资源不足");
}

int main() {
    std::future<void> fut = std::async(risky_operation);
    
    try {
        fut.get();  // 可能会抛出异常
    } catch (const std::exception& e) {
        // 在主线程中处理异常
        std::cout << "捕获到异常: " << e.what() << std::endl;
    }
    
    return 0;
}

这种机制确保了异步任务的异常不会被忽略,并且可以按照我们熟悉的方式处理。

总结

<future>库为 C++ 异步编程提供了强大而优雅的解决方案:

  1. 简化了线程返回值的获取方式,无需手动管理同步机制
  2. 提供了多种异步任务的创建方式(async、promise、packaged_task)
  3. 内置了灵活的等待机制和异常处理
  4. 让代码更加清晰、简洁、易于维护

从传统的线程 + 指针 + 锁的复杂组合,到使用 future 库的简洁代码,C++ 的异步编程体验得到了质的飞跃。掌握 future 库,能让你在多线程编程中更加得心应手,编写出更高质量的并发代码。

在实际开发中,我们可以根据具体需求选择合适的组件:简单异步任务用std::async,需要主动设置结果用std::promise,封装可重用任务用std::packaged_task

async 与 future 的关系?

std::asyncstd::future"生产者 - 消费者" 般的配套关系:std::async 负责 "生产" 异步任务的结果,std::future 负责 "消费" 这个结果,二者必须配合使用才能完成异步任务的 "启动 - 获取结果" 全流程。

简单说就是:你用 std::async 启动任务时,它会立刻给你一个 std::future 对象;后续你要拿任务结果,就靠这个 std::future 对象来获取。

🔗 核心关系:std::asyncstd::future 的 "创建者"

std::async 是 C++ 提供的 异步任务启动工具,它的核心作用有两个:

  1. 启动一个异步任务(可能新建线程,也可能延迟执行);
  2. 自动创建并返回一个 std::future 对象,这个对象直接关联到该异步任务的结果。

你可以把它们的关系理解成 "外卖下单":

  • 你用 std::async 就像 "下单点外卖"------ 发起一个需求(异步任务);
  • 平台返回的 "订单号" 就是 std::future------ 凭这个订单号,你能查进度(等待任务)、拿外卖(获取结果)。

看一段直观的代码就能明白:

cpp 复制代码
// 1. 用 std::async 启动异步任务,它会返回一个 std::future 对象
std::future<int> fut = std::async([](){ 
    // 异步任务:模拟耗时计算
    std::this_thread::sleep_for(1s);
    return 100; // 任务结果
});

// 2. 用 std::future 对象获取结果(没完成就等,完成了就拿)
int result = fut.get(); 
std::cout << "任务结果:" << result << std::endl; // 输出 100

📌 为什么不能单独用?

二者谁也离不开谁,少了一个都无法完成 "异步获取结果" 的需求:

  • 只有 std::async,没有 std::future:你启动了任务,但拿不到结果 ------ 就像下单后没要订单号,根本不知道怎么取外卖;
  • 只有 std::future,没有 std::async :你手里有个 "空的订单号"(默认构造的 std::future 是无效的),根本关联不到任何任务 ------ 没法查进度、没法拿结果。

🆚 和其他 future 来源的区别

除了 std::asyncstd::future 还能通过 std::promisestd::packaged_task 创建,但 std::async 是最 "省心" 的方式:

来源(生产者) 特点 适用场景
std::async 自动创建线程 / 管理任务,直接返回 future 简单异步任务,不想手动管理线程
std::promise 手动设置结果(set_value),需自己创建线程 需要在任务中途 / 多个地方控制结果
std::packaged_task 封装可调用对象(函数 /lambda),需自己传线程 需重复使用任务逻辑,或手动管理线程池

但无论哪种来源,std::future 的角色都是固定的 ------异步结果的 "接收者" 和 "访问接口"

std::async 与 std::future 关系对比表

该表格从职责定位、核心能力、配合流程等维度,清晰梳理二者的关联与区别,帮你快速掌握它们的协作逻辑。

对比维度 std::async(异步任务启动器) std::future(异步结果接收器) 二者关联说明
核心职责 发起异步任务,是结果的 "生产者" 存储 / 获取异步结果,是结果的 "消费者" 生产者与消费者的配套关系,缺一不可
创建方式 直接调用函数(如 std::async(任务函数) std::async/std::promise/std::packaged_task 自动生成 不能手动创建有效实例,必须通过其他组件 "赠送"
核心能力 1. 启动任务(支持立即 / 延迟两种策略)2. 自动绑定任务与 future3. 隐式管理线程生命周期 1. get():阻塞获取结果(仅能调用一次)2. wait():阻塞等待任务完成(不拿结果)3. wait_for():限时等待并返回状态 async 启动任务后,必须通过其返回的 future 才能拿到结果
配合流程 步骤 1:调用 std::async 传入任务,生成 future步骤 2:async 内部启动 / 托管任务执行 步骤 3:调用 future 的 get()/wait() 等待结果步骤 4:获取任务返回值或捕获异常 流程环环相扣,少一步都无法完成异步结果获取
生命周期 任务执行完后自动结束(或随主线程退出) 调用 get() 后变为无效(valid() 返回 false) future 失效后不可再用,async 任务完成后也无法重启
常见错误 1. 忽略返回的 future(任务会被阻塞执行)2. 重复调用启动同一任务 1. 对无效 future 调用 get()(崩溃)2. 多次调用 get()(崩溃)

promise 是不是封装了 future ?

实际上std::promisestd::future并非 "封装" 关系,而是互补的协作关系 ------ 它们就像一根管道的两端:promise是写入端,future是读取端,共同操作同一个 "共享状态"(shared state)。

可以用一个生活场景类比:

  • 你(主线程)让朋友(子线程)去买咖啡,递给朋友一个空杯子(promise
  • 朋友买到咖啡后,把咖啡倒进杯子里(promise.set_value()
  • 你拿着杯子的 "取货凭证"(future),等朋友把咖啡装好后,通过凭证拿到咖啡(future.get()

这里的关键是:杯子(共享状态)才是核心载体promisefuture只是操作这个载体的两个接口。

从实现角度看二者的关系

C++ 标准并未规定promisefuture的具体实现细节,但逻辑上它们的关系是这样的:

cpp 复制代码
+------------------------+
|      共享状态          |
|  (存储结果/异常/状态)   |
+----------+-------------+
           |
   +-------+-------+
   |               |
+--v--+         +--v--+
|promise|       |future|
|(写接口)|       |(读接口)|
+------+         +------+
  • promise拥有对共享状态的 "写权限"(设置结果 / 异常)
  • future拥有对共享状态的 "读权限"(获取结果 / 异常)
  • 两者通过promise.get_future()建立关联,形成唯一的读写对

为什么容易误解为 "封装"?

可能是因为std::packaged_taskstd::async这类工具内部封装了 promise,让我们无需直接操作它:

cpp 复制代码
// packaged_task内部包含了一个promise
std::packaged_task<int()> task([]{ return 42; });
std::future<int> fut = task.get_future(); // 这里的future依然关联内部promise的共享状态

但这是更高层工具对 promise 的封装 ,而非future封装promise。本质上,promisefuture始终是平等的协作关系,谁也不包含谁。

这种设计的精妙之处在于解耦生产者和消费者

  • 生产者只需知道如何通过promise设置结果,无需关心谁会使用这个结果
  • 消费者只需知道如何通过future获取结果,无需关心结果由谁产生
  • 两者甚至可以在不同的线程、不同的时间点创建,只要能关联到同一个共享状态即可

理解这种 "读写分离" 的设计,就能更清晰地把握 C++ 异步编程的核心思想了。

后面我们可以结合这些知识语法,自己实现一个案例!

相关推荐
一只乔哇噻2 小时前
java后端工程师进修ing(研一版‖day49)
java·开发语言
枫叶丹42 小时前
【Qt开发】输入类控件(二)-> QTextEdit
开发语言·qt
半桔3 小时前
【网络编程】TCP 粘包处理:手动序列化反序列化与报头封装的完整方案
linux·网络·c++·网络协议·tcp/ip
JAVA学习通3 小时前
微服务项目->在线oj系统(Java-Spring)----[前端]
java·开发语言·前端
hrrrrb4 小时前
【Python】文件处理(二)
开发语言·python
先知后行。5 小时前
QT实现计算器
开发语言·qt
掘根5 小时前
【Qt】常用控件3——显示类控件
开发语言·数据库·qt
GUIQU.5 小时前
【QT】嵌入式开发:从零开始,让硬件“活”起来的魔法之旅
java·数据库·c++·qt
西阳未落8 小时前
C++基础(21)——内存管理
开发语言·c++·面试