如何理解std::promise和std::future

news/2025/3/22 23:40:34/

std::promise 是 C++11 引入的一个类,用于在线程之间传递异步结果(值或异常)。它通常与 std::future 配合使用,std::promise 用于设置值或异常,而 std::future 用于获取这些值或异常。


下面通过一个更直观的生产者-消费者场景,展示 std::promise 如何在线程间传递结果:


示例:生产者线程计算平方,消费者线程获取结果

#include <iostream>
#include <thread>
#include <future>
#include <chrono>// 生产者线程:计算某个数的平方,并将结果通过 promise 传递
void producer_task(std::promise<int> result_promise, int input) {// 模拟耗时计算std::this_thread::sleep_for(std::chrono::seconds(1));// 计算平方int square = input * input;// 将结果设置到 promise 中(传递到消费者线程)result_promise.set_value(square);
}// 消费者线程:等待结果并处理
void consumer_task(std::future<int> result_future) {// 阻塞等待结果(从生产者线程获取)int result = result_future.get();// 处理结果std::cout << "消费者线程收到结果: " << result << std::endl;
}int main() {// 1. 创建 promise 和 futurestd::promise<int> prom;std::future<int> fut = prom.get_future();// 2. 启动生产者线程(计算 5 的平方)std::thread producer(producer_task, std::move(prom), 5);// 3. 启动消费者线程(处理结果)std::thread consumer(consumer_task, std::move(fut));// 等待线程结束producer.join();consumer.join();return 0;
}

代码解释

1. 生产者线程 (producer_task)
  • 输入:接收一个 std::promise<int> 和一个整数 input
  • 操作
    • 模拟耗时计算(1秒)。
    • 计算 input 的平方。
    • 将结果通过 promise.set_value() 设置到 promise 中。
  • 作用:将计算结果传递给其他线程。
2. 消费者线程 (consumer_task)
  • 输入:接收一个 std::future<int>
  • 操作
    • 调用 future.get() 阻塞等待结果。
    • 获取结果后打印。
  • 作用:从其他线程获取结果并进行处理。
3. 主线程
  • 创建 std::promise<int> 和关联的 std::future<int>
  • promise 传递给生产者线程,将 future 传递给消费者线程。
  • 等待两个线程完成。

关键点

  1. 线程间结果传递

    • 生产者线程通过 promise.set_value() 设置结果。
    • 消费者线程通过 future.get() 获取结果。
  2. 所有权转移

    • std::promisestd::future 不可复制,但可通过 std::move 转移所有权。
    • 主线程将 promise 移入生产者线程,future 移入消费者线程。
  3. 同步机制

    • future.get() 会阻塞消费者线程,直到生产者线程调用 promise.set_value()

输出结果

程序运行后输出:

消费者线程收到结果: 25

更贴近实际的应用场景

假设生产者线程需要从网络下载数据,消费者线程需要处理数据:

void downloader(std::promise<std::string> data_promise) {// 模拟下载数据std::this_thread::sleep_for(std::chrono::seconds(2));data_promise.set_value("下载完成的数据");
}void data_processor(std::future<std::string> data_future) {std::string data = data_future.get();std::cout << "处理数据: " << data << std::endl;
}int main() {std::promise<std::string> prom;std::future<std::string> fut = prom.get_future();std::thread t1(downloader, std::move(prom));std::thread t2(data_processor, std::move(fut));t1.join();t2.join();return 0;
}

总结

  • std::promise 的作用:作为生产者线程的结果出口。
  • std::future 的作用:作为消费者线程的结果入口。
  • 适用场景:需要明确分离“结果生产者”和“结果消费者”的多线程任务。

那么,如果不使用std::promise的话,两个线程间如何传递数据呢?


在 C++ 中,如果不使用 std::promisestd::future,线程间传递数据可以通过其他同步机制实现。以下是几种常见方法:


1. 共享数据 + 互斥锁 (std::mutex)

通过共享变量传递数据,使用 std::mutex 保护共享数据,防止数据竞争。

示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>std::mutex mtx;
int shared_data = 0;void producer() {std::this_thread::sleep_for(std::chrono::seconds(1));std::lock_guard<std::mutex> lock(mtx);shared_data = 42; // 生产者写入数据
}void consumer() {std::lock_guard<std::mutex> lock(mtx);std::cout << "消费者收到数据: " << shared_data << std::endl;
}int main() {std::thread t1(producer);std::thread t2(consumer);t1.join();t2.join();return 0;
}

缺点:需要手动同步,容易因锁的顺序不当导致死锁。


2. 条件变量 (std::condition_variable)

通过条件变量通知其他线程数据已就绪,通常与互斥锁和共享变量配合使用。

示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;
int shared_data = 0;void producer() {std::this_thread::sleep_for(std::chrono::seconds(1));{std::lock_guard<std::mutex> lock(mtx);shared_data = 42;data_ready = true;}cv.notify_one(); // 通知消费者
}void consumer() {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return data_ready; }); // 等待数据就绪std::cout << "消费者收到数据: " << shared_data << std::endl;
}int main() {std::thread t1(producer);std::thread t2(consumer);t1.join();t2.join();return 0;
}

优点:避免忙等待,效率更高。


3. 原子操作 (std::atomic)

对简单数据类型(如 intbool)使用原子变量,无需显式加锁。

示例:
#include <iostream>
#include <thread>
#include <atomic>std::atomic<int> atomic_data(0);void producer() {std::this_thread::sleep_for(std::chrono::seconds(1));atomic_data.store(42); // 原子写入
}void consumer() {while (atomic_data.load() == 0) { // 原子读取std::this_thread::yield(); // 避免忙等待}std::cout << "消费者收到数据: " << atomic_data.load() << std::endl;
}int main() {std::thread t1(producer);std::thread t2(consumer);t1.join();t2.join();return 0;
}

缺点:仅适用于基本数据类型,无法处理复杂对象。


4. 消息队列(生产者-消费者模式)

使用队列作为缓冲区,生产者写入数据,消费者读取数据。需配合互斥锁和条件变量。

示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>std::mutex mtx;
std::condition_variable cv;
std::queue<int> msg_queue;void producer() {for (int i = 0; i < 5; ++i) {std::this_thread::sleep_for(std::chrono::milliseconds(500));{std::lock_guard<std::mutex> lock(mtx);msg_queue.push(i);}cv.notify_one(); // 通知消费者}
}void consumer() {while (true) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [] { return !msg_queue.empty(); });int data = msg_queue.front();msg_queue.pop();std::cout << "消费者收到数据: " << data << std::endl;if (data == 4) break; // 结束条件}
}int main() {std::thread t1(producer);std::thread t2(consumer);t1.join();t2.join();return 0;
}

优点:适合处理大量数据或流式数据。


5. 回调函数(函数参数传递)

通过函数参数直接将数据传递给线程,但需注意线程的生命周期。

示例:
#include <iostream>
#include <thread>
#include <chrono>void consumer(int data) {std::cout << "消费者收到数据: " << data << std::endl;
}int main() {int data = 42;std::thread t(consumer, data); // 通过参数传递数据t.join();return 0;
}

缺点:仅适用于一次性数据传递,无法异步修改。


6. 全局变量

直接通过全局变量传递数据(需谨慎,通常不推荐)。

示例:
#include <iostream>
#include <thread>
#include <chrono>int global_data = 0;void producer() {std::this_thread::sleep_for(std::chrono::seconds(1));global_data = 42;
}void consumer() {while (global_data == 0) {std::this_thread::yield();}std::cout << "消费者收到数据: " << global_data << std::endl;
}int main() {std::thread t1(producer);std::thread t2(consumer);t1.join();t2.join();return 0;
}

缺点:不安全,易引发数据竞争。


总结

方法适用场景优点缺点
共享数据 + 互斥锁简单数据,少量访问灵活通用需手动管理锁,易死锁
条件变量需要等待特定条件高效,避免忙等待代码复杂度较高
原子操作基本数据类型,无锁需求无锁,高效仅支持基本类型
消息队列大量数据或流式处理扩展性强实现复杂
回调函数一次性数据传递简单直接无法动态更新数据
全局变量快速原型开发(不推荐生产环境)简单不安全,易引发数据竞争

根据具体需求选择合适的方法。对于异步结果传递,std::promisestd::future 是最安全和简洁的方案,但在其他场景下,上述方法可能更灵活。


http://www.ppmy.cn/news/1581252.html

相关文章

红数码影视(RED Digital Cinema)存储卡格式化后的恢复方法

红数码影视(RED Digital Cinema)的摄像机可以生成两种RAW级高清视频文件&#xff0c;一种是R3D&#xff0c;一种是MOV。其中MOV属于苹果(apple)公司的QT视频封装结构&#xff0c;使用的视频编码是Apple ProRes;而R3D则是RED公司自创的RAW视频文件&#xff0c;这种文件解码需要使…

Excel(进阶篇):powerquery详解、PowerQuery的各种用法,逆透视表格、双行表头如何制作透视表、不规则数据如何制作数据透视表

目录 PowerQuery工具基础修改现有数据理规则PowerQuery抓取数据的两种方式多文件合并透视不同表结构多表追加数据透视追加与合并整理横向表格:逆透视 数据用拆分工具整理数据算账龄 不等步长值组合合并文件夹中所有文件PowerQuery处理CSV文件双行表头、带合并单元格如何做数据…

在 Spring Boot 结合 MyBatis 的项目中,实现字段脱敏(如手机号、身份证号、银行卡号等敏感信息的部分隐藏)可以通过以下方案实现

在 Spring Boot 结合 MyBatis 的项目中&#xff0c;实现字段脱敏&#xff08;如手机号、身份证号、银行卡号等敏感信息的部分隐藏&#xff09;可以通过以下方案实现。以下是分步说明和完整代码示例&#xff1a; 一、实现方案选择 1. 方案一&#xff1a;自定义注解 Jackson 序…

迷你主机与普通台式电脑区别

一、【两者区别】 1、迷你电脑主机大部分都是采用带U的芯片&#xff0c;也就是低功耗芯片&#xff0c;整机功耗在10W-25W左右&#xff1b;普通台式机自然就是用台式机芯片&#xff0c;整机功耗约150W-300W&#xff1b; 2、迷你电脑主机大部分采用低压内存&#xff0c;也就是笔…

MSE分类时梯度消失的问题详解和交叉熵损失的梯度推导

下面是MSE不适合分类任务的解释&#xff0c;包含梯度推导。以及交叉熵的梯度推导。 前文请移步笔者的另一篇博客&#xff1a;大模型训练为什么选择交叉熵损失&#xff08;Cross-Entropy Loss&#xff09;&#xff1a;均方误差&#xff08;MSE&#xff09;和交叉熵损失的深入对比…

简述一下Unity中的碰撞检测

碰撞检测的基本工作原理 Unity中的碰撞检测是游戏开发中的核心功能&#xff0c;依赖于NVIDIA的PhysX物理引擎实现。它通过碰撞体&#xff08;Collider&#xff09;和刚体&#xff08;Rigidbody&#xff09;两个主要组件来检测和响应游戏对象之间的物理交互&#xff0c;遵循以下…

如何用Deepseek制作流程图?

使用Deepseek制作流程图&#xff0c;本质上是让AI根据你的需求&#xff0c;生成相关流程图的代码&#xff0c;然后在流程图编辑器中渲染&#xff0c;类似于Python一样&#xff0c;ChatGPT可以生成代码&#xff0c;但仍需在IDE中执行。 你知道绘制流程图最高效的工具是什么吗&a…

webpack等构建工具如何支持移除未使用的代码

Webpack 等构建工具通过 Tree Shaking&#xff08;摇树优化&#xff09;和 Dead Code Elimination&#xff08;无用代码消除&#xff09;技术来移除未使用的代码。以下是具体实现原理、配置方法及最佳实践&#xff1a; 一、Tree Shaking 的原理 Tree Shaking 是一种基于 ES Mo…