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:等待异步结果
想象一下你去机场度假的场景:
-
办理登机手续后:你完成了主要任务(登机手续),但还不能登机。
-
等待广播通知:你需要等待一个特定事件的发生(广播通知登机)。
-
打发时间:在等待期间,你可以做其他事情(看书、喝咖啡、上网)。
-
事件触发:当广播响起(事件发生),你就知道该做什么了(去登机口)。
C++ 的 future
机制与此类似:
-
**
std::future<T>
:代表一个未来**才会产生的值(类型T
)。它就像一张登机牌存根,告诉你最终会知道登机口(结果),但现在还不知道。 -
等待结果:你可以让线程在
future
上.get()
等待(就像坐着等广播),这会阻塞线程直到结果准备好。 -
做其他事:在等待期间,线程可以执行其他任务(就像你看书喝咖啡)。
-
结果就绪:当后台任务完成并设置好值后,
future
状态变为 就绪(ready)。调用.get()
会立即返回结果(就像听到广播去登机)。 -
一次性:一个
std::future
对象只能获取一次结果(.get()
只能调用一次),之后它就失效了。
C++ 提供了两种 future
:
-
**
std::future<T>
(独占式 future)**:只能被一个地方获取结果。就像只有一张登机牌存根。 -
**
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;
}
关键修改说明
-
使用自定义的
LaundryTask
结构:- 包含任务函数
taskFunc
和一个共享指针指向std::promise<std::string>
- 这样我们可以显式地控制何时设置任务结果
- 包含任务函数
-
修改了
submitLaundryTask
函数:- 显式创建
std::promise
和对应的std::future
- 将任务函数和 promise 一起封装到
LaundryTask
中并加入队列
- 显式创建
-
修改了
laundryWorker
函数:- 执行任务函数后,显式调用
set_value()
设置 promise 的结果 - 使用 try-catch 块捕获异常,并通过
set_exception()
设置异常
- 执行任务函数后,显式调用
std::promise 的作用
在这个修改后的代码中,std::promise
提供了以下功能:
- 显式设置值:员工线程完成任务后,通过
promise->set_value(result)
显式设置任务结果 - 异常传播:如果任务执行中出现异常,通过
promise->set_exception()
将异常传播到 future - 结果传递:promise 和 future 配对使用,实现了任务执行者和等待者之间的结果传递
这种方式比使用 std::packaged_task
更加灵活,因为它允许我们更精细地控制何时以及如何设置结果,特别是在复杂的异步操作中。
理解 std::promise 的作用
std::promise
是 C++ 并发编程中的一个重要工具,它允许你显式地设置一个值(或异常),这个值可以通过与之关联的 std::future
被其他线程获取。让我用一个更简单的例子来解释它的作用。
为什么需要 std::promise?
想象一下这样的场景:你有一个需要长时间运行的任务,你希望:
- 在另一个线程中执行这个任务
- 在主线程中能够获取任务的结果
- 任务可以在任何地方设置结果,而不仅仅是在函数返回时
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 的关键特性
- 显式设置值:你可以在任何地方调用
set_value()
,而不需要从函数返回 - 异常传播:可以通过
set_exception()
传播异常 - 一次性使用:一个 promise 只能设置一次值
- 与 future 配对:每个 promise 都有一个关联的 future,用于获取结果
与 std::packaged_task 的比较
特性 | std::packaged_task | std::promise |
---|---|---|
使用方式 | 包装一个函数,自动设置返回值 | 手动设置值 |
灵活性 | 较低,只能在函数返回时设置值 | 较高,可以在任何地方设置值 |
适用场景 | 简单的函数调用 | 复杂的异步操作,需要更细粒度的控制 |
为什么在洗衣店示例中使用 std::promise?
在修改后的洗衣店示例中,使用 std::promise
而不是 std::packaged_task
的原因是为了展示:
- 更明确的控制:我们可以明确地在任务完成后设置结果
- 异常处理:可以更明确地处理任务执行中的异常
- 教学目的:展示
std::promise
的基本用法
在实际应用中,如果你只是简单地包装一个函数并在函数返回时设置结果,std::packaged_task
通常更简单。但如果你需要在函数中的多个点设置结果,或者需要从多个地方设置结果,std::promise
提供了更大的灵活性。
总结
std::promise
提供了一种手动设置异步操作结果的机制,它比 std::packaged_task
更底层、更灵活。当你需要:
- 在异步操作中的任意点设置结果
- 从多个地方设置结果
- 更精细地控制异常传播
时,std::promise
是非常有用的工具。对于简单的函数包装和返回值传递,std::packaged_task
通常更简单直接。
4. 在 Future 中存储异常
如果异步任务中抛出异常,std::async
、std::packaged_task
和 std::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())
来存储当前捕获的异常。 -
如果
promise
或packaged_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
的使用:
- 共享性:
std::shared_future
可以被复制,多个线程可以共享同一个异步结果 - 转换方法:通过
std::future::share()
方法可以将std::future
转换为std::shared_future
- 多次获取:与
std::future
不同,std::shared_future
的get()
方法可以被多次调用
代码说明
-
添加了团队制服清洗任务:
washTeamJersey()
函数模拟清洗团队制服- 多个团队成员都关心这个任务的完成情况
-
使用 std::shared_future:
- 将
std::future<std::string> teamJerseyFuture
转换为std::shared_future<std::string> sharedJerseyFuture
- 这样多个线程可以共享同一个异步结果
- 将
-
创建多个团队成员线程:
- 每个团队成员线程都接收
sharedJerseyFuture
的副本 - 所有线程都可以调用
get()
方法获取结果,而不会使 future 失效
- 每个团队成员线程都接收
-
演示共享特性:
- 多个线程同时等待同一个异步结果
- 所有线程都能正确获取到结果
运行示例
当你运行这个程序时,你会看到类似以下的输出:
===== 欢迎来到全自动洗衣店 =====顾客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++ 程序的关键。