C++多线程编程(3) 异步操作类 std::future std::promise std::async
C++中提供了异步操作相关的类:
1. std::future: 异步结果的传输通道,可以很方便的获取线程函数的返回值。
在C++中,如果希望获取线程函数的返回值,就不能直接通过thread.join()得到结果,这时就必须定义一个变量,在线程函数中去给这个变量赋值,然后执行join,最后得到结果,这是一个非常繁琐的过程。C++11 的 thread 库提供了future,用来访问异步操作的结果。为什么会被命名为future呢:这是因为一个异步操作的结果不能马上获取,只能在未来某个时候从某个地方获取,这个异步操作的结果是一个未来的期待值,所以被称为future,future相当于提供了一个获取异步操作结果的通道。可以通过同步等待的方式获取结果,也可以通过查询future的状态来获取异步操作的结果:
future的状态为 future_status , 共有三种状态:
1.Deferred: 异步操作还没有开始
2. Ready: 异步操作已经完成
3. Timeout: 异步操作超时
2.std::promise:
std::promise将数据和future绑定起来,为获取线程函数中的某个值提供便利,在线程函数中为外面传进来的promise赋值,在线程函数执行完之后,就可以通过pormise的future获取该值了。取值是间接的通过promise内部提供的future进行的。
具体的关系我个人理解为如下的图:
在线程函数的外部创建std::promise,然后将它作为线程函数的参数传入,在线程函数中为其赋值。然后在线程函数外部通过promise 的 get_future()方法创建 std::future,再通过future的get方法获取变量的值即可。所以从图中可以看出,future相当于异步结果的输出通道。而这个通道是位于std::promise内部的。
使用方法如下:
使用future的get方法,获得任务执行的返回值, 但是如果当前任务尚未执行, 任务会触发立即执行, 并且堵塞当前线程,直到任务完成
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <iostream> #include <future> #include <thread> void task(std::promise<int> &prom , int para) // promise作为函数的参数 { int res = para * 10; prom.set_value_at_thread_exit(res); // 将线程中需要输出的值存放到promise中 } int main() { std::promise<int> promise_; // 创建promise std::thread t1(task, std::ref(promise_), 12); // 将promise作为参数传入到线程函数中 t1.join(); std::future<int> f = promise_.get_future(); // 创建通道 通道输出数据的类型 std::cout << "The task output " << f.get() << std::endl; return 0; }
当然,也可以在一个线程中执行线程函数,在拎一个线程中获取线程函数中需要输出的值
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <iostream> #include <future> #include <thread> void task(std::promise<int> &prom , int para) // promise作为函数的参数 { int res = para * 10; prom.set_value_at_thread_exit(res); // 将线程中需要输出的值存放到promise中 } void get_task_value(std::future<int> &future) // future作为函数的参数 { std::cout << "The task output " << future.get() << std::endl; } int main() { std::promise<int> promise_; // 创建promise std::future<int> future = promise_.get_future(); // 创建通道 std::thread t1(task, std::ref(promise_), 12); // 将promise作为参数传入到线程函数中 线程函数 std::thread t2(get_task_value, std::ref(future)); // 获取线程函数值的线程 t1.join(); t2.join(); return 0; }
可以通过查询future的状态来获取异步任务的执行情况,例如,可以在上面的代码中添加future的状态查询,直到任务完成为止。
future的状态为 future_status , 共有三种状态:
1.Deferred: 异步操作还没有开始
2. Ready: 异步操作已经完成
3. Timeout: 异步操作超时
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <iostream> #include <future> #include <thread> #include <chrono> void task(std::promise<int> &prom , int para) // promise作为函数的参数 { std::this_thread::sleep_for(std::chrono::seconds(2)); // 线程延时2s int res = para * 10; prom.set_value_at_thread_exit(res); // 将线程中需要输出的值存放到promise中 } void get_task_value(std::future<int> &future) // future作为函数的参数 { std::future_status status; do { status = future.wait_for(std::chrono::milliseconds(200)); // 等待时间 if (status == std::future_status::deferred) { // 异步操作还没有开始 std::cout << "异步操作还没有开始" << std::endl; } else if (status == std::future_status::timeout) { // 异步操作超时, 就是还没有完成的意思 std::cout << "异步操作超时" << std::endl; } else { // 异步操作已经完成 std::cout << "The task output " << future.get() << std::endl; } } while (status != std::future_status::ready); //std::cout << "The task output " << future.get() << std::endl; } int main() { std::promise<int> promise_; // 创建promise std::future<int> future = promise_.get_future(); // 创建通道 std::thread t1(task, std::ref(promise_), 12); // 将promise作为参数传入到线程函数中 线程函数 std::thread t2(get_task_value, std::ref(future)); // 获取线程函数值的线程 t1.join(); t2.join(); return 0; }
运行结果:
关于shared_future:
使用与futrue相似,shared_futrue类型允许使用第二次, 并且使用获得结果与第一次一样,如果有一场,抛出的异常也是一样的futrue共享状态在get调用后就解除,下次调用会发生报错。 但是使用shared_future的时候,get方法可调用多次,但是结果是一样的,例如:
3.std::package_task
std::package_task包装了一个可调用对象的包装类,它将函数和future绑定起来,(std::promise是将数据和future绑定起来),以便异步调用。package_task和promise有点类似,promise保存的是一个共享的状态值,package_task保存的是一个函数。(其实感觉书上的这句话并没有表述清楚)
实际上,std::future提供了一个访问异步操作结果的机制,它和线程是一个级别的,属于低层次对象。在std::future上的高一层是std::package_task和std::promise. 它们内部都有future以便访问异步操作结果。
std::promise包装的是一个异步操作,如果需要获取异步操作的返回值,就用std::package_task
std::promise包装的是一个值,如果需要获取异步操作中的某个值,就可以使用std::promise
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <iostream> #include <future> #include <thread> #include <chrono> double sum(double x, double y) { return x + y; } int main() { // 使用package_task获取返回值 std::packaged_task<double(double, double)> task(sum); // <double(double, double)>函数参数 // 获取future std::future<double> future = task.get_future(); // 需要将任务移动到线程中进行异步操作 std::thread t1(std::move(task), 2.4, 5.1); t1.join(); std::cout << "The sum is " << future.get() << std::endl; return 0; }
future的wait_for方法是超时等待返回结果,而wait方法知识等待异步操作完成,没有返回值,将上面的方法改写为wait_for方法:
例如:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <iostream> #include <future> #include <thread> #include <chrono> int factorial(int n) // 计算阶乘 { std::this_thread::sleep_for(std::chrono::milliseconds(100)); if (n == 1) return n; return n * factorial(n - 1); } void get_result(std::future<int> &future) // 获取结果 { while (future.wait_for(std::chrono::milliseconds(20)) == std::future_status::timeout) /// 等待线程结束 { std::cout << ".."; } std::cout << std::endl; std::cout << "The result is " << future.get() << std::endl; } int main() { // 使用package_task获取返回值 std::packaged_task<int(int)> task(factorial); // <double(double, double)>函数参数 // 获取future std::future<int> future = task.get_future(); // 需要将任务移动到线程中进行异步操作 std::thread t1(std::move(task), 7); // 任务处于调用线程中,移动到线程中进行处理 std::thread t2(get_result, std::ref(future)); // get_result不用move t1.join(); t2.join(); //std::cout << "The sum is " << future.get() << std::endl; system("pause"); return 0; }
线程异步操作函数async:
std::async可以用来直接创建异步的task,异步任务返回的结果保存在future中,只需要调用future.get()方法就可以获取到返回值。如果不关注异步任务的结果,则可以调用future.wait()方法,等待任务完成。
async的原型是:
std::async(std::launch::async | std::launch::deferred, f, args);
其中:
第一个参数是创建线程的方式:
std::launch::async在调用async时就创建线程。
std::launch::deferred延迟加载方式创建线程,直到调用了future的get或者wait方法时才会创建线程
第二个参数是线程函数
第三个参数是线程函数的参数
基本用法:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <iostream> #include <future> #include <thread> #include <chrono> int factorial(int n) // 计算阶乘 { std::this_thread::sleep_for(std::chrono::milliseconds(100)); if (n == 1) return n; return n * factorial(n - 1); } void func() { std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "This function has no operation" << std::endl; } int main() { std::future<int> future1 = std::async(std::launch::deferred, factorial, 5); // 线程函数有返回值 std::future<void> future2 = std::async(std::launch::async, func); // 线程函数没有参数 future2.wait(); // 调用wait() std::cout << "Factorial is " << future1.get() << std::endl; //调用get() system("pause"); return 0; }
也可以使用async来创建线程,一般也推荐这样做:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <iostream> #include <future> #include <thread> #include <chrono> void func1() { for (int i = 0; i < 1000; i++) { std::cout << "Thread Id is " << std::this_thread::get_id() << std::endl; } } int main() { std::future<void> future1 = std::async(std::launch::async, func1); // 线程函数没有参数 std::future<void> future2 = std::async(std::launch::async, func1); // 线程函数没有参数 future1.wait(); future2.wait(); // 调用wait() system("pause"); return 0; }
最后介绍一下std::shared_future
shared_future的使用与futrue相似,因为futrue共享状态在get调用后就解除,下次调用会发生报错,而shared_futrue类型允许使用第二次, 并且使用获得结果与第一次一样,如果有一场,抛出的异常也是一样的。创建方式如下:
std::shared_futrue f = std::async(task).share();
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" #include <iostream> #include <future> #include <thread> #include <chrono> int func1(int n) { return n * 10; } int main() { std::shared_future<int> future = std::async(std::launch::async, func1, 2).share(); // 线程函数没有参数 std::cout << future.get() << std::endl; std::cout << future.get() << std::endl; // 第二次调用 system("pause"); return 0; }
应该优先使用async取代线程的创建,它更加方便的实现了异步调用。
-------------------------------------------------------分割线------------------------------------------------------------
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)