C++ 线程间通信(二)

书接上文:https://blog.csdn.net/Mason___/article/details/161485948?spm=1001.2014.3001.5502

三、异步机制

线程间异步通信用于任务结果传递或非阻塞协作。‌‌

同步是互斥与条件等待(防竞态) ‌,‌**异步是任务结果回调(非阻塞通信)**‌;两者可独立或组合使用(如用 condition_variable 实现异步通知)。‌‌

异步通信(传递结果/非阻塞任务) ‌:使用 ‌**std::future / std::promise** ‌、‌**std::packaged_task** ‌ 和 ‌**std::async**‌,这些通过"共享状态"在任务线程与调用线程间传递值/异常,无需显式加锁。‌‌

std::future、std::promise、std::packaged_task 和 std::async 是 C++11 引入的并发编程组件,共同用于异步任务的结果传递和线程间通信

3.1 核心组件与作用

3.1.1 std::future

  • 作用:表示异步操作的结果,提供访问结果的机制(如等待、查询或提取值)。
  • 核心特性
    • 状态管理 :未就绪(任务未完成)、就绪(结果可用)、无效(未绑定任务或已调用get())。
    • 常用方法
      • get():阻塞获取结果(仅一次调用)。
      • wait():阻塞等待结果就绪。
      • valid():检查是否关联有效任务。
  • 局限性:
    • 单向获取std::future 仅提供 get()wait() 等方法,用于从异步操作中获取结果或等待完成,但它无法主动设置结果
    • 结果来源std::future 的结果必须由其他机制(如 std::asyncstd::packaged_taskstd::promise)生成并填充。若没有这些机制,std::future 无法独立工作。
  • 示例
cpp 复制代码
#include <iostream>
#include <future>
#include <thread>

int main() {
    std::future<int> fut = std::async([]() {
        std::this_thread::sleep_for(std::chrono::seconds(2));
        return 42;
        });

    qDebug() << "aaaaaa";
    qDebug() << "bbbbbb";
    std::future<int> fut2 = std::async([]() {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        return 51;
        });
    qDebug() << "cccccc";
    qDebug() << QString("Result: %1").arg(fut2.get()); // 阻塞获取结果
    qDebug() << QString("Result: %1").arg(fut.get()); 
    qDebug() << "dddddd";
}

结果:

cpp 复制代码
aaaaaa
bbbbbb
cccccc
"Result: 51"
"Result: 42"
dddddd

3.1.2 std::promise ( + std::future

其核心作用是为一个线程(生产者)提供存储结果(值或异常)的机制,并通过关联的 std::future 对象让另一个线程(消费者)异步获取该结果。

3.1.2.1 作用
  • 主动设置结果std::promise 是异步操作的"生产者",通过 set_value()set_exception() 等方法主动设置结果或异常。
  • std::future 绑定 :每个 std::promise 对象通过 get_future() 返回一个关联的 std::future 对象,后者用于在另一线程中获取结果。
  • 线程间通信std::promise 允许一个线程设置值,另一个线程通过 std::future 获取值,实现跨线程的结果传递。
3.1.2.2 核心特性
  • 结果设置
    • set_value(const T& value):设置结果值。
    • set_exception(std::exception_ptr e):设置异常。
  • 关联futureget_future()生成关联的std::future(仅一次调用)。
  • 生命周期管理 :若未设置结果,关联的std::future会抛出std::future_errorbroken promise异常)。
3.1.2.3 问题讲解

有人会问我直接用std::future不也能获取结果吗?为什么要std::promise配合?

  • 第一,std::future 的结果必须由其他机制(如 std::asyncstd::packaged_taskstd::promise)生成并填充。若没有这些机制,std::future 无法独立工作。
  • 第二,**std::async**自动管理线程和结果传递,但灵活性较低(例如无法动态设置结果)。
cpp 复制代码
auto fut = std::async([] { return 42; }); // 内部使用 std::promise 或类似机制
int result = fut.get();
  • std::packaged_task :将可调用对象与结果存储绑定,但仍需通过 std::promise 或类似机制设置结果。
cpp 复制代码
std::packaged_task<int()> task([] { return 42; });
std::future<int> fut = task.get_future();
std::thread(std::move(task)).detach(); // 任务在线程中执行
int result = fut.get();
3.1.2.4 std::promise作用
  • 控制结果的生产时机

例如,在多线程任务中,生产者线程可能需要根据条件动态生成结果,而 std::promise 提供了灵活的设置接口。

cpp 复制代码
std::promise<int> prom;
std::future<int> fut = prom.get_future();

std::thread producer([&prom] {
    int result = compute_result(); // 动态计算结果
    prom.set_value(result);       // 主动设置结果
});

int value = fut.get(); // 消费者线程获取结果
producer.join();
  • 传递异常

std::promise 可以通过 set_exception() 将异常从生产者线程传递到消费者线程的 std::future

cpp 复制代码
std::promise<void> prom;
std::future<void> fut = prom.get_future();

std::thread worker([&prom] {
    try {
        risky_operation();
        prom.set_value(); // 成功时设置空值
    } catch (...) {
        prom.set_exception(std::current_exception()); // 传递异常
    }
});

fut.get(); // 若发生异常,会在此处重新抛出
worker.join();
  • 解耦生产与消费

    std::promisestd::future 的分离允许生产者和消费者独立运行,无需直接共享数据,避免竞态条件。

3.1.2.5 总结
  • std::future 是异步结果的"消费者",用于获取结果或等待完成。
  • std::promise 是异步结果的"生产者",用于主动设置结果或异常。
  • 二者配合std::promise 提供结果的生产接口,std::future 提供结果的消费接口,共同实现线程间安全的结果传递。

通过这种设计,C++ 标准库提供了灵活且线程安全的异步编程模型,适用于复杂的多线程场景。

3.1.3 std::packaged_task ( + std::future

std::packaged_task 是 C++11 引入的类模板,用于将可调用对象(函数、lambda、仿函数等)包装为异步任务,并通过 std::future 获取其返回值或异常。

3.1.3.1 模板定义
cpp 复制代码
template<class R, class... Args> 
class packaged_task<R(Args...)>;
  • R : 可调用对象的返回类型(支持任意类型,包括 void)。
  • Args...: 可调用对象的参数类型包(支持 0 个或多个参数)。
  • 示例:
cpp 复制代码
std::packaged_task<int(int, int)> task([](int a, int b) { return a + b; });
3.1.3.2 内部组成
  • 存储的任务(Stored Task): 封装可调用对象(函数、lambda 等),调用签名需与模板参数匹配。
  • 共享状态(Shared State) : 存储任务结果或异常,通过 std::future 访问,线程安全。
3.1.3.3 核心工作机制
  • 包装任务 : 初始化 std::packaged_task,绑定可调用对象。
  • 绑定 future : 调用 get_future() 获取关联的 std::future
  • 执行任务 : 通过 operator()make_ready_at_thread_exit() 触发任务。
  • 获取结果 : 通过 future::get() 阻塞等待结果或异常。
3.1.3.4 成员函数详解
成员函数 功能 关键特性
构造函数 包装可调用对象 支持移动构造,无拷贝构造;默认构造无效(valid() == false
get_future() 获取关联的 std::future 每个 packaged_task 仅能调用一次,否则抛出 std::future_error
operator()(Args...) 同步执行任务 需传入匹配参数;执行后立即置共享状态为就绪
make_ready_at_thread_exit(Args...) 延迟状态就绪至线程退出 适用于确保线程资源释放后再通知其他线程的场景
reset() 重置任务状态 销毁旧共享状态,创建新状态;可重新调用 get_future()
valid() 检查有效性 默认构造/移动后/reset() 后的对象返回 false
swap() 交换两个任务对象 轻量级操作,仅交换内部指针
3.1.3.5 使用示例
【1】基本用法(Lambda 表达式)
cpp 复制代码
#include <future>
#include <iostream>

int main() {
    std::packaged_task<int(int, int)> task([](int a, int b) { 
        return a + b; 
    });

    std::future<int> result = task.get_future(); // 绑定 future
    task(2, 3); // 同步执行任务
    std::cout << "Result: " << result.get() << std::endl; // 输出: 5
    return 0;
}

意思是支持operator()纯同步,虽然用它的目的是去支持异步(结果与任务分离)。

【2】异步执行(结合 std::thread

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

int main() {
    std::packaged_task<int(int, int)> task([](int a, int b) { 
        return a * b; 
    });

    std::future<int> result = task.get_future();
    std::thread t(std::move(task), 4, 5); // 移动任务到线程
    t.join();
    std::cout << "Result: " << result.get() << std::endl; // 输出: 20
    return 0;
}

【3】重置任务

std::packaged_task::reset() 的核心作用是重置任务状态,使其可以重新执行并生成新的结果 。这一机制常用于需要重复执行异步任务的场景(如线程池任务调度),但需注意及时更新关联的 std::future 对象以避免访问失效状态。

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

int task(int x, int y) {
    return x + y;
}

int main() {
    // 1. 创建 packaged_task 并绑定任务
    std::packaged_task<int(int, int)> pt(task);
    std::future<int> fut = pt.get_future(); // 获取关联的 future

    // 2. 第一次执行任务(同步)
    pt(2, 3);
    std::cout << "First result: " << fut.get() << std::endl; // 输出 5

    // 3. 重置 packaged_task
    pt.reset(); // 销毁旧共享状态,创建新状态
    fut = pt.get_future(); // 必须重新获取 future

    // 4. 第二次执行任务(通过线程异步执行)
    std::thread t(std::move(pt), 4, 5);
    t.join();
    std::cout << "Second result: " << fut.get() << std::endl; // 输出 9

    return 0;
}


输出:
First result: 5
Second result: 9
3.1.3.6 与 std::asyncstd::function 的对比
特性 std::packaged_task<R(Args...)> std::async std::function<R(Args...)>
核心定位 可调用对象的异步包装器(绑定共享状态) 异步执行函数的快捷工具 可调用对象的通用包装器(无异步)
任务执行时机 手动控制(operator() 或线程) 自动控制(立即或延迟) 手动调用(operator()
共享状态 内置(与 future 绑定) 自动创建(隐式关联)
任务复用性 支持(reset() 后可重用) 不支持(一次性任务) 支持(可重复调用)
灵活性 高(完全控制线程/时机) 低(封装细节) 中(仅包装,无异步)
3.1.3.7 适用场景
  • 手动控制异步任务:需自定义线程管理或任务调度(如线程池)。
  • 任务传递:将任务对象传递到其他线程执行。
  • 结果异步获取 :通过 std::future 实现线程同步和结果获取。

总结std::packaged_task 是连接可调用对象与 C++ 异步编程体系的核心桥梁,适合需要精细控制任务执行的场景,而 std::async 更适合快速实现简单异步操作。

3.1.4 std::async

std::async 是 C++11 引入的异步任务工具,用于简化多线程编程,通过返回 std::future 对象实现异步结果获取和任务管理。

3.1.4.1 核心功能
  1. 异步执行任务

    • 可能在新线程中执行(取决于启动策略),也可能延迟执行。
    • 返回 std::future 对象,用于获取结果或管理任务状态。
  2. 结果获取与异常处理

    • future.get():阻塞等待任务完成,返回结果或抛出任务中的异常。
    • future.wait():仅阻塞等待任务完成,不获取结果。
  3. 任务状态查询

    • future.valid():检查 future 是否关联有效结果。
    • future.wait_for(timeout)/future.wait_until(timepoint):超时或定时等待。
3.1.4.2 启动策略
策略 说明
std::launch::async 强制在新线程中异步执行任务。
std::launch::deferred 延迟执行,直到调用 future.get()wait() 时在当前线程同步执行。
默认策略 **未显式指定策略,等同于 `std::launch::async
3.1.4.3 基础用法示例
【1】最简用法(无显式策略)
cpp 复制代码
#include <iostream>
#include <future>

int compute(int a, int b) {
    return a + b;
}

int main() {
    auto future = std::async(compute, 2, 3); // 启动异步任务
    std::cout << future.get() << std::endl;   // 阻塞等待结果(输出 5)
    return 0;
}
【2】显式指定启动策略
cpp 复制代码
auto future_async = std::async(std::launch::async, compute, 2, 3); // 强制异步
auto future_deferred = std::async(std::launch::deferred, compute, 2, 3); // 延迟执行
future_deferred.get(); // 此时在当前线程同步执行 compute(2, 3)
【3】传递引用参数

需用 std::ref 包装引用,避免值拷贝:

cpp 复制代码
#include <functional>
void modify(int& value) {
    value = 100;
}

int main() {
    int x = 0;
    auto future = std::async(modify, std::ref(x)); // 传递引用
    future.get();
    std::cout << x << std::endl; // 输出 100
    return 0;
}
【4】异常处理

异步任务中的异常会被 std::future 捕获,直到调用 get() 时重新抛出:

cpp 复制代码
int risky_compute(int x) {
    if (x < 0) throw std::runtime_error("x is negative");
    return x * 2;
}

int main() {
    auto future = std::async(risky_compute, -1);
    try {
        int result = future.get(); // 抛出 std::runtime_error
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl; // 捕获异常
    }
    return 0;
}
3.1.4.4 注意事项

1. 生命周期管理

  • std::future 析构时,若任务未完成且未被 get()wait(),可能导致程序终止(取决于实现)。
  • 若需共享结果,使用 std::shared_future

2. 线程复用

  • std::async 不保证任务一定在新线程中执行(尤其是 deferred 策略或编译器优化)。

3. 性能开销

  • 频繁创建小任务可能有额外开销,适合中高耗时任务。
3.1.4.5 典型应用场景
  • 并行计算:同时执行多个独立任务(如图像处理中的多区域计算)。
  • 异步 I/O:发起 I/O 操作后异步等待结果,避免阻塞主线程。
  • 任务编排 :结合 future.wait_for 实现超时控制(如监控任务是否超时)。
3.1.4.6 总结

std::async 是 C++ 中简化异步编程的核心工具,通过 std::future 提供结果获取和状态查询能力。合理使用启动策略和异常处理机制,可有效提升代码的并发性能和可维护性。

3.2 对比与选择

工具 适用场景 特点
std::promise 需要显式设置值或异常,并手动管理线程间通信。 灵活,但需手动管理生命周期和线程同步。
std::packaged_task 需要将函数包装为异步任务,并手动调度到线程。 适合复杂任务调度,但需显式移动任务对象。
std::async 快速启动异步任务,无需关心线程管理。 简单易用,但默认行为可能因编译器实现而异(如 std::launch 策略)。

3.3 注意事项

线程安全

  • std::promiseset_value()set_exception() 不是线程安全的,需确保同一对象不被多线程同时修改。
  • std::futureget() 只能调用一次,多次调用会导致未定义行为。

异常处理

  • std::promise 设置异常,std::future::get() 会重新抛出该异常,需用 try-catch 捕获。

资源管理

  • 确保 std::promise 在设置结果前未被销毁,否则 std::future::get() 会抛出 std::future_error

性能优化

  • 短任务避免使用 std::async,因线程创建开销可能超过任务执行时间。
  • 高并发场景考虑使用线程池(如 Intel TBB 或自定义实现)。

3.4 总结

  • std::promise + std::future:适合需要显式控制值传递和线程同步的场景。
  • std::packaged_task:适合需要包装函数并手动调度任务的场景。
  • std::async:适合快速启动异步任务,简化代码的场景。

根据需求选择合适的工具,可以高效实现线程间通信和异步任务管理。

相关推荐
minji...1 小时前
Linux 高级IO(七)多进程、多线程的Reactor反应堆模式扩展、OTOL
linux·运维·c++·多路转接·epoll·reactor反应堆模型
晚风吹红霞1 小时前
C++ list 容器完全指南:从入门到手撕双向链表
c++·链表·list
handler011 小时前
【Linux 网络】:poll/epoll 底层机制与 Reactor 并发模型
linux·运维·服务器·网络·c++·多路转接·多路复用
cpp_25011 小时前
P10109 [GESP202312 六级] 工作沟通
数据结构·c++·算法·题解·洛谷·gesp六级
Xeon_CC1 小时前
vs2026远程开发debian12容器的C++程序笔记
开发语言·c++·笔记
玉树临风ives1 小时前
atcoder ABC 460 题解
数据结构·c++·算法
少司府1 小时前
C++进阶:二叉搜索树
开发语言·数据结构·c++·二叉树·stl·二叉搜索树·tree
Huangjin007_1 小时前
【C++ STL篇(十四)】哈希表实现:开放定址法与链地址法
c++·哈希算法·散列表
承渊政道1 小时前
【MySQL数据库学习】MySQL表的约束(上)
数据库·c++·学习·mysql·bash·数据库架构·数据库系统