当前位置: 首页 > news >正文

C++并发编程指南14 使用future

文章目录

      • 理解 Future:等待异步结果
      • 1. 获取后台任务的结果 (`std::async`)
      • 2. 将任务与 Future 关联 (`std::packaged_task`)
      • 3. 显式设置值 (`std::promise`)
      • 使用 std::promise 的洗衣店模拟程序
      • 关键修改说明
      • std::promise 的作用
  • 理解 std::promise 的作用
    • 为什么需要 std::promise?
    • 简单示例
    • std::promise 的关键特性
    • 与 std::packaged_task 的比较
    • 为什么在洗衣店示例中使用 std::promise?
    • 总结
      • 4. 在 Future 中存储异常
      • 5. 多个线程等待同一个结果 (`std::shared_future`)
  • 使用 std::shared_future 的洗衣店模拟程序
    • std::shared_future 的关键特性
    • 代码说明
    • 运行示例
      • 总结

理解 Future:等待异步结果

想象一下你去机场度假的场景:

  1. 办理登机手续后​:你完成了主要任务(登机手续),但还不能登机。

  2. 等待广播通知​:你需要等待一个特定事件的发生(广播通知登机)。

  3. 打发时间​:在等待期间,你可以做其他事情(看书、喝咖啡、上网)。

  4. 事件触发​:当广播响起(事件发生),你就知道该做什么了(去登机口)。

C++ 的 future机制与此类似:

  • ​**std::future<T>​:代表一个未来**才会产生的值(类型 T)。它就像一张登机牌存根,告诉你最终会知道登机口(结果),但现在还不知道。

  • 等待结果​:你可以让线程在 future.get()等待(就像坐着等广播),这会阻塞线程直到结果准备好。

  • 做其他事​:在等待期间,线程可以执行其他任务(就像你看书喝咖啡)。

  • 结果就绪​:当后台任务完成并设置好值后,future状态变为 ​就绪(ready)​。调用 .get()会立即返回结果(就像听到广播去登机)。

  • 一次性​:一个 std::future对象只能获取一次结果(.get()只能调用一次),之后它就失效了。

C++ 提供了两种 future

  1. ​**std::future<T>(独占式 future)​**​:只能被一个地方获取结果。就像只有一张登机牌存根。

  2. ​**std::shared_future<T>(共享式 future)​**​:可以被复制,多个地方可以等待并获取同一个结果。就像广播通知,所有候机乘客都能听到。

它们都定义在 <future>头文件中。

1. 获取后台任务的结果 (std::async)

最常见的方式是使用 std::async启动一个后台任务并获取其 future

#include <iostream>
#include <future>
#include <thread>
#include <chrono>// 一个耗时的计算函数(模拟)
int calculateAnswer() {std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时计算return 42; // 宇宙的终极答案
}void doOtherTasks() {for (int i = 0; i < 5; ++i) {std::cout << "Doing other task " << i << "..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(500));}
}int main() {// 使用 std::async 启动异步任务,获取 future// 默认启动策略:由库决定是异步执行还是延迟执行std::future<int> answerFuture = std::async(calculateAnswer);// 主线程继续执行其他任务(不阻塞)doOtherTasks();// 现在需要计算结果了,等待 future 就绪并获取结果// .get() 会阻塞,直到 calculateAnswer() 完成并返回值int theAnswer = answerFuture.get();std::cout << "The answer is: " << theAnswer << std::endl;return 0;
}

输出示例:​

Doing other task 0...
Doing other task 1...
Doing other task 2...
Doing other task 3...
Doing other task 4...
The answer is: 42

关键点:​

  • std::async返回一个 std::future,持有函数 calculateAnswer的返回值。

  • doOtherTasks()在主线程与后台计算并发执行。

  • answerFuture.get()阻塞主线程,直到 calculateAnswer完成并返回结果。

std::async传递参数:​

#include <future>
#include <string>struct Worker {void processData(int id, const std::string& data) {// 处理数据...}std::string generateReport(const std::string& title) {// 生成报告...return "Report: " + title;}
};int main() {Worker worker;// 传递成员函数指针、对象指针和参数auto f1 = std::async(&Worker::processData, &worker, 101, "Important Data");// 传递成员函数指针、对象拷贝和参数auto f2 = std::async(&Worker::generateReport, worker, "Quarterly Results");std::string report = f2.get(); // 等待并获取报告字符串// 传递函数对象 (仿函数)struct Multiplier {double operator()(double x) { return x * 2.5; }};Multiplier mult;auto f3 = std::async(Multiplier(), 10.0); // 移动构造 Multiplier 临时对象auto f4 = std::async(std::ref(mult), 20.0); // 传递 mult 的引用// 传递普通函数double complexCalculation(double input);auto f5 = std::async(complexCalculation, 15.7);// 传递只移动类型 (move-only) 的函数对象struct MoveOnlyTask {void operator()() { /* ... */ }MoveOnlyTask() = default;MoveOnlyTask(MoveOnlyTask&&) = default; // 移动构造MoveOnlyTask(const MoveOnlyTask&) = delete; // 不可拷贝};auto f6 = std::async(MoveOnlyTask()); // 移动构造return 0;
}

控制 std::async的执行策略:​

#include <future>
#include <iostream>void task() {std::cout << "Task running in thread: " << std::this_thread::get_id() << std::endl;
}int main() {// 策略 1: std::launch::async - 强制在新线程异步执行auto f1 = std::async(std::launch::async, task);// 策略 2: std::launch::deferred - 延迟执行,直到调用 get() 或 wait()auto f2 = std::async(std::launch::deferred, task);std::cout << "f2 created. Calling f2.get()..." << std::endl;f2.get(); // 此时 task() 才会在当前线程执行// 策略 3: 默认策略 (std::launch::async | std::launch::deferred)//         由库实现决定是异步还是延迟auto f3 = std::async(task); // 默认策略f1.wait(); // 确保 f1 完成f3.wait(); // 确保 f3 完成return 0;
}

2. 将任务与 Future 关联 (std::packaged_task)

std::packaged_task是一个可调用对象包装器。它将一个函数或可调用对象包装起来,并将其返回值(或异常)存储在与之关联的 future中。它本身不启动线程,需要你手动执行它(例如,在另一个线程中调用它,或放入任务队列)。

场景:​​ 非常适合构建任务队列或线程池,任务调度器只需要处理 std::packaged_task对象,而不关心具体函数细节。

#include <iostream>
#include <future>
#include <thread>
#include <deque>
#include <mutex>
#include <functional>
#include <random>
#include <chrono>
#include <string>// 洗衣任务队列 (全局)
std::deque<std::packaged_task<std::string()>> laundryQueue;
std::mutex queueMutex;// 洗衣店员工函数 (模拟)
void laundryWorker() {std::random_device rd;std::mt19937 gen(rd());std::uniform_int_distribution<> dis(1000, 3000); // 模拟洗衣时间随机性while (true) {// 模拟员工休息一下std::this_thread::sleep_for(std::chrono::milliseconds(500));std::packaged_task<std::string()> task;{// 锁定队列以检查任务std::lock_guard<std::mutex> lock(queueMutex);if (laundryQueue.empty()) {std::cout << "员工: 暂时没有衣物需要清洗,我先休息一会...\n";continue;}// 取出队列最前面的任务task = std::move(laundryQueue.front());laundryQueue.pop_front();}// 模拟洗衣需要的时间std::cout << "员工: 开始清洗衣物...\n";std::this_thread::sleep_for(std::chrono::milliseconds(dis(gen)));// 执行洗衣任务 - 这里只需要调用 task(),不需要获取返回值// 返回值会自动存储到与 packaged_task 关联的 promise 中task();std::cout << "员工: 一件衣物清洗完成!\n";}
}// 提交洗衣任务的函数
template <typename Func>
std::future<std::string> submitLaundryTask(Func func) {// 用传入的函数创建 packaged_taskstd::packaged_task<std::string()> task(func);// 获取与该任务关联的 futurestd::future<std::string> future = task.get_future();{// 锁定队列以添加任务std::lock_guard<std::mutex> lock(queueMutex);// 将任务移动到队列尾部laundryQueue.push_back(std::move(task));std::cout << "系统: 新的洗衣任务已加入队列\n";}// 返回 future,顾客可以用它等待任务完成并获取结果return future;
}// 示例洗衣任务函数 - 返回洗衣名称
std::string washJacket() {std::cout << "洗衣机: 正在清洗夹克...\n";return "蓝色夹克";
}std::string washJeans() {std::cout << "洗衣机: 正在清洗牛仔裤...\n";return "黑色牛仔裤";
}std::string washShirts() {std::cout << "洗衣机: 正在清洗衬衫...\n";return "白色衬衫";
}int main() {std::cout << "===== 欢迎来到全自动洗衣店 =====\n";// 启动洗衣店员工线程std::thread workerThread(laundryWorker);workerThread.detach(); // 分离线程// 顾客1提交洗衣任务std::cout << "\n顾客1: 我要洗我的夹克!\n";std::future<std::string> task1Future = submitLaundryTask(washJacket);// 顾客2提交洗衣任务std::cout << "\n顾客2: 我要洗我的牛仔裤!\n";std::future<std::string> task2Future = submitLaundryTask(washJeans);// 顾客3提交洗衣任务std::cout << "\n顾客3: 我要洗我的衬衫!\n";std::future<std::string> task3Future = submitLaundryTask(washShirts);// 顾客们可以做其他事情std::cout << "\n所有顾客: 我们去购物中心逛逛,衣服洗好了再回来...\n";// 模拟顾客做其他事情std::this_thread::sleep_for(std::chrono::seconds(2));// 顾客们回来等待洗衣完成并获取结果std::cout << "\n顾客们: 我们回来取衣服了!\n";// 顾客1获取洗衣结果std::string jacketResult = task1Future.get();std::cout << "顾客1: 拿到了我的" << jacketResult << ",洗得真干净!\n";// 顾客2获取洗衣结果std::string jeansResult = task2Future.get();std::cout << "顾客2: 拿到了我的" << jeansResult << ",焕然一新!\n";// 顾客3获取洗衣结果std::string shirtsResult = task3Future.get();std::cout << "顾客3: 拿到了我的" << shirtsResult << ",洁白如新!\n";std::cout << "\n===== 洗衣店今日营业结束 =====\n";return 0;
}

这段代码是一个很好的示例,展示了如何使用std::packaged_task和std::future来实现一个任务队列,模拟洗衣店的工作流程。代码中,顾客提交洗衣任务(夹克、牛仔裤、衬衫),洗衣店员工(线程)从队列中取出任务并执行,顾客可以通过future获取洗衣完成的结果(衣物名称)。
代码优点:

1.​解耦清晰​:顾客(主线程)和员工(工作线程)完全解耦。顾客只需要提交任务并获取future,不需要关心任务如何执行、由谁执行。员工只负责从队列中取任务并执行。
2.​使用标准库组件​:合理使用了std::packaged_task、std::future、std::mutex、std::deque等标准库组件,展示了现代C++并发编程的风格。
3.​异常安全​:虽然代码中没有显式处理异常,但std::packaged_task在执行时会自动捕获异常并传递给future,顾客在调用get()时可能会收到异常。这符合设计。
4.​模拟现实场景​:通过随机睡眠模拟洗衣时间的不确定性,增加了真实感。
5.​资源管理​:使用std::lock_guard管理互斥锁,确保队列操作的线程安全。
6.​任务封装​:std::packaged_task将任务(函数)和结果(future)打包,使得任务可以异步执行,结果可以延迟获取。
7.​代码可读性​:函数命名清晰,注释得当,逻辑清晰。

潜在改进点:

1.​线程终止​:员工线程(laundryWorker)是一个无限循环,没有退出机制。在实际应用中,应该提供一种方式(如设置一个停止标志)来优雅地停止线程。否则,程序结束时线程还在运行,可能导致问题。
2.​队列无任务时的忙等待​:当队列为空时,员工线程会不断循环检查(每次循环休息500毫秒)。这虽然不是严格的忙等待(因为休息了),但仍有改进空间。可以使用条件变量(std::condition_variable)来让员工线程在队列为空时等待,直到有新任务加入时被唤醒。这样可以减少CPU占用。
3.​任务执行顺序​:代码中使用了队列,所以任务是按提交顺序执行的(FIFO)。这符合洗衣店的场景,但如果有优先级任务,可能需要使用优先队列。
4.​多个员工线程​:目前只有一个员工线程。在实际洗衣店中,可能有多个员工同时工作。可以扩展为线程池,处理并发任务。
5.​异常处理​:在员工线程中执行任务(task())时,如果任务函数抛出异常,packaged_task会捕获并存储异常,然后通过future传递给顾客。但员工线程本身没有捕获异常,如果任务函数抛出异常,员工线程会继续运行(因为packaged_task的执行已经处理了异常)。这没有问题,但可以添加日志记录异常。
6.​返回值处理​:在员工线程中,执行任务后并没有使用返回值,因为返回值已经通过packaged_task的内部机制存储到关联的promise中了。但代码中有一行输出“一件衣物清洗完成”,这里可以获取任务返回值并输出,但注意这样会阻塞员工线程直到任务完成(实际上任务已经完成,只是获取返回值)。例如:std::string item = task.get_future().get(); // 不推荐,因为已经通过task()执行了,再get会阻塞,但实际已经就绪
std::cout << "员工: " << item << “清洗完成!\n”;
但这样写会多一次get调用,而且任务已经执行过了,直接输出清洗完成即可,不需要再获取返回值(返回值是给顾客的)。
7.​任务函数设计​:任务函数(如washJacket)中输出了清洗信息,这模拟了洗衣过程。但注意,这些输出是在员工线程中执行的,可能会与主线程的输出交错。如果对输出顺序有要求,可能需要同步输出。

总结:
这段代码很好地展示了std::packaged_task和std::future在任务队列中的应用,体现了异步任务提交和结果获取的分离。通过一个洗衣店的例子,生动地说明了生产者(顾客)-消费者(员工)模式。同时,代码结构清晰,易于理解。
改进方向主要是增加线程终止机制和优化任务调度(如使用条件变量避免忙等待)。对于更复杂的场景,可以考虑扩展为线程池和任务优先级队列。

3. 显式设置值 (std::promise)

std::promise提供了最底层的方式来设置 future的值(或异常)。你创建一个 promise,从它获取一个 future,然后在某个时刻(可能在另一个线程中)调用 promise.set_value()来设置结果,这会立即使关联的 future变为就绪状态。

场景:​​ 适用于需要将结果从多个地方、复杂逻辑或非函数调用上下文中传递出来的情况。例如,处理网络连接事件。

使用 std::promise 的洗衣店模拟程序

下面是一个修改后的洗衣店模拟程序,其中显式使用了 std::promise 来设置洗衣任务的结果,而不是依赖 std::packaged_task 的自动设置:

#include <iostream>
#include <future>
#include <thread>
#include <deque>
#include <mutex>
#include <functional>
#include <random>
#include <chrono>
#include <string>// 洗衣任务结构,包含任务函数和对应的promise
struct LaundryTask {std::function<std::string()> taskFunc;std::shared_ptr<std::promise<std::string>> taskPromise;
};// 洗衣任务队列 (全局)
std::deque<LaundryTask> laundryQueue;
std::mutex queueMutex;// 洗衣店员工函数 (模拟)
void laundryWorker() {std::random_device rd;std::mt19937 gen(rd());std::uniform_int_distribution<> dis(1000, 3000); // 模拟洗衣时间随机性while (true) {// 模拟员工休息一下std::this_thread::sleep_for(std::chrono::milliseconds(500));LaundryTask task;{// 锁定队列以检查任务std::lock_guard<std::mutex> lock(queueMutex);if (laundryQueue.empty()) {std::cout << "员工: 暂时没有衣物需要清洗,我先休息一会...\n";continue;}// 取出队列最前面的任务task = std::move(laundryQueue.front());laundryQueue.pop_front();}// 模拟洗衣需要的时间std::cout << "员工: 开始清洗衣物...\n";std::this_thread::sleep_for(std::chrono::milliseconds(dis(gen)));try {// 执行洗衣任务并显式设置promise的值std::string result = task.taskFunc();task.taskPromise->set_value(result);std::cout << "员工: 一件衣物清洗完成!\n";} catch (...) {// 如果任务执行中发生异常,将异常设置到promise中task.taskPromise->set_exception(std::current_exception());}}
}// 提交洗衣任务的函数
template <typename Func>
std::future<std::string> submitLaundryTask(Func func) {// 创建一个promise和对应的futureauto promise = std::make_shared<std::promise<std::string>>();std::future<std::string> future = promise->get_future();{// 锁定队列以添加任务std::lock_guard<std::mutex> lock(queueMutex);// 将任务和promise添加到队列中laundryQueue.push_back({func, promise});std::cout << "系统: 新的洗衣任务已加入队列\n";}// 返回future,顾客可以用它等待任务完成并获取结果return future;
}// 示例洗衣任务函数 - 返回洗衣名称
std::string washJacket() {std::cout << "洗衣机: 正在清洗夹克...\n";return "蓝色夹克";
}std::string washJeans() {std::cout << "洗衣机: 正在清洗牛仔裤...\n";return "黑色牛仔裤";
}std::string washShirts() {std::cout << "洗衣机: 正在清洗衬衫...\n";return "白色衬衫";
}int main() {std::cout << "===== 欢迎来到全自动洗衣店 =====\n";// 启动洗衣店员工线程std::thread workerThread(laundryWorker);workerThread.detach(); // 分离线程// 顾客1提交洗衣任务std::cout << "\n顾客1: 我要洗我的夹克!\n";std::future<std::string> task1Future = submitLaundryTask(washJacket);// 顾客2提交洗衣任务std::cout << "\n顾客2: 我要洗我的牛仔裤!\n";std::future<std::string> task2Future = submitLaundryTask(washJeans);// 顾客3提交洗衣任务std::cout << "\n顾客3: 我要洗我的衬衫!\n";std::future<std::string> task3Future = submitLaundryTask(washShirts);// 顾客们可以做其他事情std::cout << "\n所有顾客: 我们去购物中心逛逛,衣服洗好了再回来...\n";// 模拟顾客做其他事情std::this_thread::sleep_for(std::chrono::seconds(2));// 顾客们回来等待洗衣完成并获取结果std::cout << "\n顾客们: 我们回来取衣服了!\n";// 顾客1获取洗衣结果std::string jacketResult = task1Future.get();std::cout << "顾客1: 拿到了我的" << jacketResult << ",洗得真干净!\n";// 顾客2获取洗衣结果std::string jeansResult = task2Future.get();std::cout << "顾客2: 拿到了我的" << jeansResult << ",焕然一新!\n";// 顾客3获取洗衣结果std::string shirtsResult = task3Future.get();std::cout << "顾客3: 拿到了我的" << shirtsResult << ",洁白如新!\n";std::cout << "\n===== 洗衣店今日营业结束 =====\n";return 0;
}

关键修改说明

  1. 使用自定义的 LaundryTask 结构

    • 包含任务函数 taskFunc 和一个共享指针指向 std::promise<std::string>
    • 这样我们可以显式地控制何时设置任务结果
  2. 修改了 submitLaundryTask 函数

    • 显式创建 std::promise 和对应的 std::future
    • 将任务函数和 promise 一起封装到 LaundryTask 中并加入队列
  3. 修改了 laundryWorker 函数

    • 执行任务函数后,显式调用 set_value() 设置 promise 的结果
    • 使用 try-catch 块捕获异常,并通过 set_exception() 设置异常

std::promise 的作用

在这个修改后的代码中,std::promise 提供了以下功能:

  1. 显式设置值:员工线程完成任务后,通过 promise->set_value(result) 显式设置任务结果
  2. 异常传播:如果任务执行中出现异常,通过 promise->set_exception() 将异常传播到 future
  3. 结果传递:promise 和 future 配对使用,实现了任务执行者和等待者之间的结果传递

这种方式比使用 std::packaged_task 更加灵活,因为它允许我们更精细地控制何时以及如何设置结果,特别是在复杂的异步操作中。

理解 std::promise 的作用

std::promise 是 C++ 并发编程中的一个重要工具,它允许你显式地设置一个值(或异常),这个值可以通过与之关联的 std::future 被其他线程获取。让我用一个更简单的例子来解释它的作用。

为什么需要 std::promise?

想象一下这样的场景:你有一个需要长时间运行的任务,你希望:

  1. 在另一个线程中执行这个任务
  2. 在主线程中能够获取任务的结果
  3. 任务可以在任何地方设置结果,而不仅仅是在函数返回时

std::promise 就是为解决这种情况而设计的。

简单示例

#include <iostream>
#include <thread>
#include <future>
#include <chrono>void taskFunction(std::promise<int> resultPromise) {// 模拟一些工作std::this_thread::sleep_for(std::chrono::seconds(2));// 显式设置结果resultPromise.set_value(42);// 注意:这里不需要返回任何值
}int main() {// 创建一个 promise 对象std::promise<int> resultPromise;// 获取与 promise 关联的 futurestd::future<int> resultFuture = resultPromise.get_future();// 启动线程执行任务,传递 promisestd::thread worker(taskFunction, std::move(resultPromise));// 在主线程中做其他工作...std::cout << "主线程正在做其他工作..." << std::endl;// 当需要结果时,等待并获取它int result = resultFuture.get();std::cout << "任务结果是: " << result << std::endl;worker.join();return 0;
}

std::promise 的关键特性

  1. 显式设置值:你可以在任何地方调用 set_value(),而不需要从函数返回
  2. 异常传播:可以通过 set_exception() 传播异常
  3. 一次性使用:一个 promise 只能设置一次值
  4. 与 future 配对:每个 promise 都有一个关联的 future,用于获取结果

与 std::packaged_task 的比较

特性std::packaged_taskstd::promise
使用方式包装一个函数,自动设置返回值手动设置值
灵活性较低,只能在函数返回时设置值较高,可以在任何地方设置值
适用场景简单的函数调用复杂的异步操作,需要更细粒度的控制

为什么在洗衣店示例中使用 std::promise?

在修改后的洗衣店示例中,使用 std::promise 而不是 std::packaged_task 的原因是为了展示:

  1. 更明确的控制:我们可以明确地在任务完成后设置结果
  2. 异常处理:可以更明确地处理任务执行中的异常
  3. 教学目的:展示 std::promise 的基本用法

在实际应用中,如果你只是简单地包装一个函数并在函数返回时设置结果,std::packaged_task 通常更简单。但如果你需要在函数中的多个点设置结果,或者需要从多个地方设置结果,std::promise 提供了更大的灵活性。

总结

std::promise 提供了一种手动设置异步操作结果的机制,它比 std::packaged_task 更底层、更灵活。当你需要:

  1. 在异步操作中的任意点设置结果
  2. 从多个地方设置结果
  3. 更精细地控制异常传播

时,std::promise 是非常有用的工具。对于简单的函数包装和返回值传递,std::packaged_task 通常更简单直接。

4. 在 Future 中存储异常

如果异步任务中抛出异常,std::asyncstd::packaged_taskstd::promise都能捕获这个异常并将其存储在关联的 future中。当调用 future.get()时,存储的异常会被重新抛出。

 #include <iostream>
#include <future>
#include <thread>
#include <deque>
#include <mutex>
#include <functional>
#include <random>
#include <chrono>
#include <string>
#include <stdexcept>// 洗衣任务队列 (全局)
std::deque<std::packaged_task<std::string()>> laundryQueue;
std::mutex queueMutex;// 洗衣店员工函数 (模拟)
void laundryWorker() {std::random_device rd;std::mt19937 gen(rd());std::uniform_int_distribution<> dis(1000, 3000); // 模拟洗衣时间随机性while (true) {// 模拟员工休息一下std::this_thread::sleep_for(std::chrono::milliseconds(500));std::packaged_task<std::string()> task;{// 锁定队列以检查任务std::lock_guard<std::mutex> lock(queueMutex);if (laundryQueue.empty()) {std::cout << "员工: 暂时没有衣物需要清洗,我先休息一会...\n";continue;}// 取出队列最前面的任务task = std::move(laundryQueue.front());laundryQueue.pop_front();}// 模拟洗衣需要的时间std::cout << "员工: 开始清洗衣物...\n";std::this_thread::sleep_for(std::chrono::milliseconds(dis(gen)));// 执行洗衣任务 - 这里只需要调用 task(),不需要获取返回值// 如果任务抛出异常,异常会被自动捕获并存储在关联的 future 中task();std::cout << "员工: 一件衣物清洗完成!\n";}
}// 提交洗衣任务的函数
template <typename Func>
std::future<std::string> submitLaundryTask(Func func) {// 用传入的函数创建 packaged_taskstd::packaged_task<std::string()> task(func);// 获取与该任务关联的 futurestd::future<std::string> future = task.get_future();{// 锁定队列以添加任务std::lock_guard<std::mutex> lock(queueMutex);// 将任务移动到队列尾部laundryQueue.push_back(std::move(task));std::cout << "系统: 新的洗衣任务已加入队列\n";}// 返回 future,顾客可以用它等待任务完成并获取结果return future;
}// 示例洗衣任务函数 - 返回洗衣名称
std::string washJacket() {std::cout << "洗衣机: 正在清洗夹克...\n";return "蓝色夹克";
}std::string washJeans() {std::cout << "洗衣机: 正在清洗牛仔裤...\n";return "黑色牛仔裤";
}std::string washShirts() {std::cout << "洗衣机: 正在清洗衬衫...\n";return "白色衬衫";
}// 一个可能会抛出异常的洗衣任务
std::string washDelicateItem() {std::cout << "洗衣机: 正在清洗精致衣物...\n";// 模拟随机故障 - 有30%的概率会出问题std::random_device rd;std::mt19937 gen(rd());std::uniform_int_distribution<> dis(1, 10);if (dis(gen) <= 9) { // 90%的概率throw std::runtime_error("洗衣机故障: 精致衣物被洗坏了!");}return "精致丝绸衬衫";
}int main() {std::cout << "===== 欢迎来到全自动洗衣店 =====\n";// 启动洗衣店员工线程std::thread workerThread(laundryWorker);workerThread.detach(); // 分离线程// 顾客1提交洗衣任务std::cout << "\n顾客1: 我要洗我的夹克!\n";std::future<std::string> task1Future = submitLaundryTask(washJacket);// 顾客2提交洗衣任务std::cout << "\n顾客2: 我要洗我的牛仔裤!\n";std::future<std::string> task2Future = submitLaundryTask(washJeans);// 顾客3提交可能会抛出异常的任务std::cout << "\n顾客3: 我要洗我的精致丝绸衬衫!\n";std::future<std::string> task3Future = submitLaundryTask(washDelicateItem);// 顾客们可以做其他事情std::cout << "\n所有顾客: 我们去购物中心逛逛,衣服洗好了再回来...\n";// 模拟顾客做其他事情std::this_thread::sleep_for(std::chrono::seconds(2));// 顾客们回来等待洗衣完成并获取结果std::cout << "\n顾客们: 我们回来取衣服了!\n";// 顾客1获取洗衣结果try {std::string jacketResult = task1Future.get();std::cout << "顾客1: 拿到了我的" << jacketResult << ",洗得真干净!\n";}catch (const std::exception& e) {std::cout << "顾客1: 糟糕! " << e.what() << std::endl;}// 顾客2获取洗衣结果try {std::string jeansResult = task2Future.get();std::cout << "顾客2: 拿到了我的" << jeansResult << ",焕然一新!\n";}catch (const std::exception& e) {std::cout << "顾客2: 糟糕! " << e.what() << std::endl;}// 顾客3获取洗衣结果 - 这里可能会捕获到异常try {std::string delicateResult = task3Future.get();std::cout << "顾客3: 拿到了我的" << delicateResult << ",完美如新!\n";}catch (const std::exception& e) {std::cout << "顾客3: 糟糕! " << e.what() << std::endl;std::cout << "顾客3: 我要投诉! 这件衣服很贵的!\n";}std::cout << "\n===== 洗衣店今日营业结束 =====\n";return 0;
}

输出:​

===== 欢迎来到全自动洗衣店 =====顾客1: 我要洗我的夹克!
系统: 新的洗衣任务已加入队列顾客2: 我要洗我的牛仔裤!
系统: 新的洗衣任务已加入队列顾客3: 我要洗我的精致丝绸衬衫!
系统: 新的洗衣任务已加入队列所有顾客: 我们去购物中心逛逛,衣服洗好了再回来...
员工: 开始清洗衣物...顾客们: 我们回来取衣服了!
洗衣机: 正在清洗夹克...
员工: 一件衣物清洗完成!
顾客1: 拿到了我的蓝色夹克,洗得真干净!
员工: 开始清洗衣物...
洗衣机: 正在清洗牛仔裤...
员工: 一件衣物清洗完成!
顾客2: 拿到了我的黑色牛仔裤,焕然一新!
员工: 开始清洗衣物...
洗衣机: 正在清洗精致衣物...
员工: 一件衣物清洗完成!
顾客3: 糟糕! 洗衣机故障: 精致衣物被洗坏了!
顾客3: 我要投诉! 这件衣服很贵的!===== 洗衣店今日营业结束 =====

关键点:​

  • 三种方式 (async, packaged_task, promise) 都能将任务中抛出的异常传递回调用 future.get()的线程。

  • 使用 promise时,可以在 catch块中显式调用 set_exception(std::current_exception())来存储当前捕获的异常。

  • 如果 promisepackaged_task在设置值或异常之前就被销毁了,关联的 future.get()会抛出 std::future_error异常,错误码为 std::future_errc::broken_promise

5. 多个线程等待同一个结果 (std::shared_future)

std::future是独占的(只能移动,不能复制)。如果多个线程需要等待同一个异步结果,就需要使用 std::shared_future。它可以被复制,每个副本都引用同一个共享状态。

使用 std::shared_future 的洗衣店模拟程序

下面我将修改洗衣店模拟程序,展示如何使用 std::shared_future 来允许多个线程等待同一个异步结果。在这个例子中,我将创建一个场景,其中多个顾客(线程)都关心同一件衣物的清洗结果。

#include <iostream>
#include <future>
#include <thread>
#include <deque>
#include <mutex>
#include <functional>
#include <random>
#include <chrono>
#include <string>
#include <vector>// 洗衣任务队列 (全局)
std::deque<std::packaged_task<std::string()>> laundryQueue;
std::mutex queueMutex;// 洗衣店员工函数 (模拟)
void laundryWorker() {std::random_device rd;std::mt19937 gen(rd());std::uniform_int_distribution<> dis(1000, 3000); // 模拟洗衣时间随机性while (true) {// 模拟员工休息一下std::this_thread::sleep_for(std::chrono::milliseconds(500));std::packaged_task<std::string()> task;{// 锁定队列以检查任务std::lock_guard<std::mutex> lock(queueMutex);if (laundryQueue.empty()) {std::cout << "员工: 暂时没有衣物需要清洗,我先休息一会...\n";continue;}// 取出队列最前面的任务task = std::move(laundryQueue.front());laundryQueue.pop_front();}// 模拟洗衣需要的时间std::cout << "员工: 开始清洗衣物...\n";std::this_thread::sleep_for(std::chrono::milliseconds(dis(gen)));// 执行洗衣任务task();std::cout << "员工: 一件衣物清洗完成!\n";}
}// 提交洗衣任务的函数
template <typename Func>
std::future<std::string> submitLaundryTask(Func func) {// 用传入的函数创建 packaged_taskstd::packaged_task<std::string()> task(func);// 获取与该任务关联的 futurestd::future<std::string> future = task.get_future();{// 锁定队列以添加任务std::lock_guard<std::mutex> lock(queueMutex);// 将任务移动到队列尾部laundryQueue.push_back(std::move(task));std::cout << "系统: 新的洗衣任务已加入队列\n";}// 返回 future,顾客可以用它等待任务完成并获取结果return future;
}// 示例洗衣任务函数 - 返回洗衣名称
std::string washJacket() {std::cout << "洗衣机: 正在清洗夹克...\n";return "蓝色夹克";
}std::string washJeans() {std::cout << "洗衣机: 正在清洗牛仔裤...\n";return "黑色牛仔裤";
}std::string washShirts() {std::cout << "洗衣机: 正在清洗衬衫...\n";return "白色衬衫";
}// 一个特殊的洗衣任务 - 清洗团队制服
std::string washTeamJersey() {std::cout << "洗衣机: 正在清洗团队制服...\n";return "红色团队制服";
}// 团队成员函数 - 每个成员都关心制服的清洗结果
void teamMember(const std::string& name, std::shared_future<std::string> jerseyFuture) {std::cout << name << ": 我正在等待我们的制服清洗完成...\n";// 做其他事情...std::this_thread::sleep_for(std::chrono::milliseconds(500));// 等待制服清洗完成std::string result = jerseyFuture.get();std::cout << name << ": 太好了! 我们的" << result << "已经洗好了,可以参加比赛了!\n";
}int main() {std::cout << "===== 欢迎来到全自动洗衣店 =====\n";// 启动洗衣店员工线程std::thread workerThread(laundryWorker);workerThread.detach(); // 分离线程// 顾客1提交洗衣任务std::cout << "\n顾客1: 我要洗我的夹克!\n";std::future<std::string> task1Future = submitLaundryTask(washJacket);// 顾客2提交洗衣任务std::cout << "\n顾客2: 我要洗我的牛仔裤!\n";std::future<std::string> task2Future = submitLaundryTask(washJeans);// 团队提交制服清洗任务std::cout << "\n队长: 我们要洗我们的团队制服!\n";std::future<std::string> teamJerseyFuture = submitLaundryTask(washTeamJersey);// 将 std::future 转换为 std::shared_futurestd::shared_future<std::string> sharedJerseyFuture = teamJerseyFuture.share();// 创建多个团队成员线程,它们都关心同一件制服的清洗结果std::vector<std::thread> teamMembers;std::vector<std::string> memberNames = {"队员A", "队员B", "队员C", "队员D"};for (const auto& name : memberNames) {teamMembers.emplace_back(teamMember, name, sharedJerseyFuture);}// 顾客们可以做其他事情std::cout << "\n所有顾客: 我们去购物中心逛逛,衣服洗好了再回来...\n";// 模拟顾客做其他事情std::this_thread::sleep_for(std::chrono::seconds(2));// 顾客们回来等待洗衣完成并获取结果std::cout << "\n顾客们: 我们回来取衣服了!\n";// 顾客1获取洗衣结果std::string jacketResult = task1Future.get();std::cout << "顾客1: 拿到了我的" << jacketResult << ",洗得真干净!\n";// 顾客2获取洗衣结果std::string jeansResult = task2Future.get();std::cout << "顾客2: 拿到了我的" << jeansResult << ",焕然一新!\n";// 等待所有团队成员完成for (auto& thread : teamMembers) {thread.join();}std::cout << "\n===== 洗衣店今日营业结束 =====\n";return 0;
}

std::shared_future 的关键特性

在这个修改后的代码中,我展示了 std::shared_future 的使用:

  1. 共享性std::shared_future 可以被复制,多个线程可以共享同一个异步结果
  2. 转换方法:通过 std::future::share() 方法可以将 std::future 转换为 std::shared_future
  3. 多次获取:与 std::future 不同,std::shared_futureget() 方法可以被多次调用

代码说明

  1. 添加了团队制服清洗任务

    • washTeamJersey() 函数模拟清洗团队制服
    • 多个团队成员都关心这个任务的完成情况
  2. 使用 std::shared_future

    • std::future<std::string> teamJerseyFuture 转换为 std::shared_future<std::string> sharedJerseyFuture
    • 这样多个线程可以共享同一个异步结果
  3. 创建多个团队成员线程

    • 每个团队成员线程都接收 sharedJerseyFuture 的副本
    • 所有线程都可以调用 get() 方法获取结果,而不会使 future 失效
  4. 演示共享特性

    • 多个线程同时等待同一个异步结果
    • 所有线程都能正确获取到结果

运行示例

当你运行这个程序时,你会看到类似以下的输出:

===== 欢迎来到全自动洗衣店 =====顾客1: 我要洗我的夹克!
系统: 新的洗衣任务已加入队列顾客2: 我要洗我的牛仔裤!
系统: 新的洗衣任务已加入队列队长: 我们要洗我们的团队制服!
系统: 新的洗衣任务已加入队列队员A: 我正在等待我们的制服清洗完成...
队员B: 我正在等待我们的制服清洗完成...
队员C: 我正在等待我们的制服清洗完成...
队员D: 我正在等待我们的制服清洗完成...所有顾客: 我们去购物中心逛逛,衣服洗好了再回来...员工: 开始清洗衣物...
员工: 一件衣物清洗完成!
员工: 开始清洗衣物...
员工: 一件衣物清洗完成!
员工: 开始清洗衣物...
员工: 一件衣物清洗完成!顾客们: 我们回来取衣服了!顾客1: 拿到了我的蓝色夹克,洗得真干净!
顾客2: 拿到了我的黑色牛仔裤,焕然一新!
队员A: 太好了! 我们的红色团队制服已经洗好了,可以参加比赛了!
队员B: 太好了! 我们的红色团队制服已经洗好了,可以参加比赛了!
队员C: 太好了! 我们的红色团队制服已经洗好了,可以参加比赛了!
队员D: 太好了! 我们的红色团队制服已经洗好了,可以参加比赛了!===== 洗衣店今日营业结束 =====

这个示例展示了 std::shared_future 如何允许多个线程共享同一个异步结果,这在需要多个消费者等待同一个生产者的场景中非常有用。

总结

C++ 的 future机制 (std::future, std::shared_future, std::async, std::packaged_task, std::promise) 提供了强大的工具来处理异步操作和线程间结果传递:

  • ​**std::async:​**​ 最简单的方式启动异步任务并获取 future

  • ​**std::packaged_task:​**​ 将任务包装成一个可调用对象,方便放入队列或传递给线程,并关联 future

  • ​**std::promise:​**​ 最底层、最灵活的方式,允许在任意位置显式设置 future的值或异常。

  • 异常处理:​​ 异步任务中的异常会自动或手动存储到关联的 future中,并在 get()时重新抛出。

  • 多线程等待:​​ 使用 std::shared_future让多个线程安全地等待和获取同一个异步结果。

理解这些组件及其相互关系,是编写高效、健壮的并发 C++ 程序的关键。


文章转载自:

http://lOa9bRCW.msgnx.cn
http://EGxlnYP9.msgnx.cn
http://wKsTHt1x.msgnx.cn
http://b7eZDuPM.msgnx.cn
http://Pk7CAdQV.msgnx.cn
http://DUv42d5C.msgnx.cn
http://sLIz9lq7.msgnx.cn
http://0iq6FkKw.msgnx.cn
http://cJr8sutc.msgnx.cn
http://cM01alNH.msgnx.cn
http://NEkIHFaG.msgnx.cn
http://J9D7tDq1.msgnx.cn
http://9FhNRm9W.msgnx.cn
http://2X4eH0YJ.msgnx.cn
http://RyRR3Rfh.msgnx.cn
http://zXdMucVJ.msgnx.cn
http://O24eodqk.msgnx.cn
http://LPrtkTZy.msgnx.cn
http://adtIjDTW.msgnx.cn
http://O6WJugG4.msgnx.cn
http://N6ALpATH.msgnx.cn
http://oRRfkAw4.msgnx.cn
http://tYJB4SmN.msgnx.cn
http://Pd1l3nUM.msgnx.cn
http://2g8l3ynV.msgnx.cn
http://3QOj60mF.msgnx.cn
http://KGg7aTgb.msgnx.cn
http://pz6ElfKC.msgnx.cn
http://KFKp1dij.msgnx.cn
http://gFJpyyLp.msgnx.cn
http://www.dtcms.com/a/364256.html

相关文章:

  • MySQL知识点3
  • 电子病历空缺句的语言学特征描述与自动分类探析(以GPT-5为例)(中)
  • Cookie、Session、登录
  • 空调噪音不穿帮,声网虚拟直播降噪技巧超实用
  • 【论文阅读】-《THE JPEG STILL PICTURE COMPRESSION STANDARD》
  • 【论文阅读】LightThinker: Thinking Step-by-Step Compression (EMNLP 2025)
  • 自然语言处理深层语义分析中公理化体系的可行性、挑战与前沿进展
  • 华为HCIP、HCIE认证:自学与培训班的抉择
  • 《探索C++11:现代语法的性能优化策略(中篇)》
  • 分布式系统的 CAP 原则与 BASE 原则理解
  • 精密板料矫平机:让金属“心平气和”的科技
  • 多场景对练数据的 Excel 横向导出方案(EasyExcel 动态表头实践)
  • Django REST Framework Serializer 进阶教程
  • 少儿舞蹈小程序(6)打造您的“活”名片:动态展示机构实力
  • lesson53:CSS五种定位方式全解析:从基础到实战应用
  • 20250901 搜索总结
  • C语言中的运算符
  • vue3 使用css变量
  • CSS Sass Less 样式.xxx讲解
  • 代码随想录算法训练营第四天|链表part02
  • Windows 10/11 系统 vcruntime140.dll 故障终极解决:从重装组件到系统修复的完整流程
  • 飞算JavaAI真能帮小白搞定在线图书借阅系统?开发效果大揭秘!
  • shell中命令小工具:cut、sort、uniq,tr的使用方式
  • 电子电气架构 --- 新EEA架构下开发模式转变
  • Redis基础概述
  • 分词器详解(一)
  • 第二十章 ESP32S3 IIC_EEPROM 实验
  • STM32 - Embedded IDE - GCC - 使用 GCC 链接脚本限制 Flash 区域
  • 【Android】从复用到重绘的控件定制化方式
  • React实现音频文件上传与试听