C++并发与多线程(八) async、future、packaged_task、promise

本文详细介绍了C++中std::async、std::future、std::launch、std::packaged_task和std::promise等并发控制工具的使用。std::async用于启动异步任务,返回std::future,后者提供了获取异步结果的机制。std::launch参数决定任务何时执行,std::packaged_task用于包装可调用对象,而std::promise则允许在不同线程间传递和设置值。通过这些工具,开发者可以灵活地管理后台任务和数据交换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

std::async、std::future创建后台任务并返回值

std::async

std::async是一个函数模板,用来启动一个异步任务,启动起来一个异步任务之后,它返回一个std::future对象,这个对象是个类模板。

异步任务
自动创建一个线程,并开始 执行对应的线程入口函数,它返回一个std::future对象,这个std::future对象中就含有线程入口函数所返回的结果(其实就是线程执行的结果),我们可以通过调用future对象的成员函数get()来获取结果。

std::future

std::future提供了一种访问异步操作结果的机制,就是说这个结果你可能没办法马上拿到,但是在不久的将来,这个线程执行完毕的时候,你就能够拿到结果了,所以,可以理解未:future中保存着一个值,这个值是在将来的某个时刻能够拿到。

  1. std::future.get()成员函数会等待线程执行结束并返回结果,拿不到结果它就会一直等待,感觉有点像join()但是,它是可以获取结果的。

  2. std::future.wait()成员函数,用于等待线程返回,本身并不返回结果,这个效果和 std::thread 的join()更像。

代码示例:

#include <future>

//线程入口函数
int myThread() {
	cout << "thread begin with id =" << this_thread::get_id() << endl; //打印线程id
	chrono::microseconds dura(5000); //休息5s
	this_thread::sleep_for(dura); //休息了一定时间
	cout << "thread end with id =" << this_thread::get_id() << endl; //打印线程id

	return 5;
}

int main() {
	cout << "main thread id = " << this_thread::get_id() << endl;
	future<int> result = async(myThread); //创建线程,入口函数为myThread

	cout << result.get() << endl; //调用future的get成员函数获取结果并打印
								  //如果myThread没执行完,会一直在这里等待,直到子进程执行结束,拿到返回结果

	return 0;
}
  1. 包含头文件#include < future >
  2. future<int> result = async(myThread); 创建一个线程,入口函数为myThread,future类模板,返回结果类型为int
  3. result.get()获得返回的结果,如果myThread没执行完,会一直在这里等待,直到子进程执行结束,拿到返回结果。(因此必须子线程的入口函数必须有返回值,不然主进程会一直卡在这里)
  4. get()成员函数只能调用一次,调用多次会报错。
  5. 如果不调用get(),主线程也会在return 0;之前一直等待子进程执行完毕(但是推荐使用get,这样写出的程序稳定)

std::launch

我们通过向std::async()传递一个参数,该参数是std::launch类型(枚举类型),来达到一些特殊的目的:

1.std::lunch::deferred
std::lunch::deferred表示线程入口函数的调用会被延迟,一直到std::future的wait()或者get()函数被调用时(由主线程调用)才会执行;如果wait()或者get()没有被调用,则不会执行。

实际上根本就没有创建新线程。std::lunch::deferred意思时延迟调用,并没有创建新线程,是在主线程中调用的线程入口函数。

代码示意:

#include <future>

//用类作线程入口函数
class MyThread {
public:
	int myThread(int val) {
		cout << "thread begin with id =" << this_thread::get_id() << endl; //打印线程id

		cout << "thread val =" << val << endl;

		chrono::microseconds dura(5000); //休息5s
		this_thread::sleep_for(dura); //休息了一定时间
		cout << "thread end with id =" << this_thread::get_id() << endl; //打印线程id

		return 5;
	}
};
int main() {
	cout << "main thread id = " << this_thread::get_id() << endl;
//	future<int> result = async(myThread); //创建线程,入口函数为myThread

	MyThread a;
	int tempval = 12;
	future<int> result = async(std::launch::deferred, &MyThread::myThread, &a, tempval); //创建线程,用类的成员函数作线程入口函数,注意传参的个数


	cout << result.get() << endl; //调用future的get成员函数获取结果并打印
								  //如果myThread没执行完,会一直在这里等待,直到子进程执行结束,拿到返回结果

	return 0;
}

在这里插入图片描述

  1. 这里是用的类的成员函数作线程入口函数:注意async的调用:future<int> result = async(std::launch::deferred, &MyThread::myThread, &a, tempval);参数的传参顺序与方式
  2. 可以看到结果,子进程与主进程的id一样,说明根本没有创建新进程;

所以deferred实际上就是延迟调用,不创建新线程,在主线程中执行入口函数。

2.std::launch::async

在调用async函数的时候就开始创建新线程。也就是说,不用等到调用get,在创建future的时候,就已经可以运行新线程了。

	MyThread a;
	int tempval = 12;
	future<int> result = async(std::launch::async, &MyThread::myThread, &a, tempval); //创建线程,用类的成员函数作线程入口函数,注意传参的个数

	cout << "loading!" << endl;
	cout << "loading!" << endl;
	cout << "loading!" << endl;
	cout << "loading!" << endl;
	cout << "loading!" << endl;

	cout << result.get() << endl; //调用future的get成员函数获取结果并打印
								  //如果myThread没执行完,会一直在这里等待,直到子进程执行结束,拿到返回结果

可以用这段代码测试,可以发现,main还在打印loading的时候,就已经进入myThread开始执行了。

std::packaged_task

packaged_task:打包任务,把任务包装起来。它也是一个类模板,它的模板参数是各种可调用对象,通过packaged_task把各种可调用对象包装起来,方便将来作为线程入口函数来调用。

	packaged_task<int(int)> mypt(myThread); //<int(int)>表示一个返回值为int,参数为int的函数,用packaged_task进行包装
	thread mythread(ref(mypt), 1); //创建一个新线程,线程入口函数为packaged_pack包装的,参数为1的函数
	mythread.join();
	future<int> result = mypt.get_future();//std::future对象里包含有线程入口函数的返回结果,这里result保存mythread返回的结果。
	cout << result.get() << endl;

packaged_task包装一个lamda表达式:

int main() {
	std::packaged_task<int(int)> mypt([](int mypar) {
		cout << mypar << endl;
		cout << "mythread() start" << "threadid = " << std::this_thread::get_id() << endl;
		std::chrono::milliseconds dura(5000);
		std::this_thread::sleep_for(dura);
		cout << "mythread() end" << "threadid = " << std::this_thread::get_id() << endl;
		return 5;
	}); 
	
	std::thread mythread(std::ref(mypt), 1);
	mythread.join();
	std::future<int> result = mypt.get_future(); 
	//std::future对象里包含有线程入口函数的返回结果,这里result保存mythread返回的结果。
	cout << result.get() << endl;
	return 0;
}

std::promise

std::promise也是一个类模板,能够在某个线程中给它赋值,然后我们可以在其他线程中,把这个值取出来。

void myThread(promise<int> &temp, int calc) {
	//做一系列运算
	calc++;

	chrono::microseconds dura(5000); //休息5s
	this_thread::sleep_for(dura); //休息了一定时间,用sleep模拟运算时间

	//计算结果
	int result = calc;
	temp.set_value(result); //用set_value把结果保存到promise对象中

}

int main() {
	cout << "main thread id = " << this_thread::get_id() << endl;

	promise<int> myprom; //实例化一个promise对象,保存值为int
	thread mythread(myThread, ref(myprom), 180); //注意传参:线程入口函数,参数1,参数2
	mythread.join();

	//获取结果值
	future<int> result = myprom.get_future(); //promise和future绑定,用于获取返回值
	cout << result.get() << endl;

	return 0;
}
  1. 通过promise保存一个值,在将来某个时刻我们通过吧一个future绑定到这个promise上,来得到绑定的值;
  2. 注意使用thread时,必须 join() 或者 detach() 否则程序会报异常;
  3. 因此,我们可以通过promise + future的方式实现两个线程数据的交换,线程2可以使用线程1计算出的结果。

对于3,代码如下:

//myThread见上面代码
void myThread2(future<int> &temp) {
	auto result = temp.get();
	cout << "thread2 result = " << result << endl;
}
int main() {
	promise<int> myprom; //实例化一个promise对象,保存值为int
	thread mythread(myThread, ref(myprom), 180); //注意传参:线程入口函数,参数1,参数2
	mythread.join();

	//获取结果值
	future<int> result = myprom.get_future(); //promise和future绑定,用于获取返回值

	//线程2使用线程1计算出的结果
	thread mythread2(myThread2, ref(result));
	mythread2.join();

	return 0;
}

最后:

  1. 我们学习这些东西的目的并不是,要把他们都用到实际开发中。

  2. 相反,如果我们能够用最少的东西写出一个稳定的,高效的多线程程序,更值得赞赏。

  3. 我们为了成长必须阅读一些高手写的代码,从而实现自己代码的积累;

<think>嗯,用户问的是`packaged_task`是否相当于把函数调用和返回值的获取分离开来。首先,我需要回顾一下`std::packaged_task`的基本概念和用途。根据之前的知识,`packaged_task`确实用于将函数或可调用对象封装起来,并通过`get_future()`方法获取之关联的`future`对象,从而异步获取结果。 用户可能是在学习多线程编程,特别是在使用C++并发工具时遇到了`packaged_task`。他们可能已经了解过`std::future`和`std::promise`,现在想理解`packaged_task`的作用和设计意图。用户的深层需求可能是想理解如何有效地将任务执行结果处理分离,以便编写更清晰、高效的并发代码。 接下来,我需要确认`packaged_task`是否确实分离了函数调用和返回值获取。根据之前的回答,`packaged_task`确实将任务封装起来,允许在另一个线程中执行,并通过`future`获取结果。这符合分离的概念,因为任务的执行和结果的获取是通过不同的对象(`packaged_task`和`future`)处理的,可能在不同的线程中。 需要进一步解释为什么这种分离是有用的。例如,分离允许将任务传递给线程池,然后在需要时获取结果,而不必阻塞主线程。这可能涉及到`packaged_task``std::async`或`std::thread`的对比,强调`packaged_task`提供了更多的控制,比如手动管理线程和执行时机。 另外,用户可能对`packaged_task``promise`的区别有疑问。需要指出,`promise`需要手动设置值,而`packaged_task`自动将函数返回值传递给关联的`future`,简化了流程。 还要注意用户可能的误区,比如是否每次调用`get()`都会执行任务,或者`packaged_task`的生命周期问题。例如,`packaged_task`只能被移动,不能复制,确保任务只被执行一次。 总结来说,用户需要明确的答案是`packaged_task`确实分离了调用和结果获取,并且需要了解这种分离如何工作、它的优势以及其他并发工具的区别。回答时需要结构清晰,分点说明,可能结合代码示例和实际应用场景来加强理解。</think>是的,`std::packaged_task` 的核心设计目标正是**将函数的调用返回值的获取分离**,这种分离是多线程异步编程的关键机制。以下是分层解析其工作原理和意义: --- ### 一、核心分离机制 #### 1. 职责划分 - **调用端**:通过移动(`move`)或直接调用`packaged_task`对象触发函数执行。 - **结果端**:通过关联的`std::future`对象(由`get_future()`获取)异步提取返回值。 #### 2. 典型流程 ```mermaid sequenceDiagram participant A as 主线程 participant B as 子线程 participant P as packaged_task participant F as future A->>P: 创建 packaged_task A->>P: 调用 get_future() → 绑定到 F A->>B: 将 P 移动至子线程 B->>P: 执行封装的函数(触发计算) P->>F: 自动存储返回值 A->>F: 通过 get() 获取结果 ``` --- ### 二、分离的具体表现 #### 1. 调用结果的物理隔离 - **调用位置**:可能在子线程中执行(如 `std::thread` 中移动 `packaged_task`)。 - **结果获取**:可在主线程或其他任意位置通过`future`异步等待结果。 #### 2. 代码示例对比 ```cpp // 传统同步调用(无分离) string ret = TestPack(100); // 直接阻塞调用 // packaged_task 分离调用结果 packaged_task<string(int)> task(TestPack); future<string> result = task.get_future(); thread th(move(task), 101); // 任务在子线程执行 // ... 其他操作 ... string ret = result.get(); // 异步获取结果 th.join(); ``` --- ### 三、分离的意义优势 #### 1. 异步化控制 - **非阻塞调用**:主线程无需等待函数执行完成,可继续处理其他任务。 - **延迟获取结果**:仅在需要时才通过`future.get()`阻塞等待结果。 #### 2. 资源解耦 - **线程池适配**:可将`packaged_task`提交到线程池,由池管理线程资源。 - **任务传递**:任务对象可跨线程移动,实现灵活的并发调度。 #### 3. 错误隔离 - **异常传递**:函数中的异常会通过`future.get()`重新抛出,调用线程隔离。 --- ### 四、其他机制的对比 | 机制 | 调用结果绑定 | 适用场景 | |---------------------|----------------|----------------------------------| | `普通函数调用` | 直接同步返回 | 简单单线程逻辑 | | `std::async` | 隐式分离 | 快速启动异步任务(自动管理线程) | | `std::packaged_task`| 显式分离 | 需要精细控制任务调度的场景 | | `std::promise` | 手动分离 | 需完全手动控制结果传递的复杂逻辑 | --- ### 五、实际应用场景 #### 1. 生产者-消费者模型 ```cpp queue<packaged_task<string()>> task_queue; // 任务队列 // 生产者线程 packaged_task<string()> task([]{ return generate_data(); }); auto result = task.get_future(); task_queue.push(move(task)); // 消费者线程 auto task = task_queue.pop(); task(); // 执行任务 ``` #### 2. 批量任务调度 ```cpp vector<future<string>> results; for (int i=0; i<10; ++i) { packaged_task<string(int)> task(TestPack); results.emplace_back(task.get_future()); thread_pool.submit(move(task), i); // 提交到线程池 } // 统一收集结果 for (auto& fut : results) { cout << fut.get() << endl; } ``` --- ### 总结 `std::packaged_task` 通过将**函数调用**和**结果载体**(`future`)分离,实现了: - **异步任务调度**:灵活控制任务执行时机 - **结果安全传递**:通过类型安全的`future`通道 - **资源解耦**:支持跨线程的任务移动 这种分离是多线程编程中“**Fire-and-Forget**”(触发后不管)模式的基础,为构建高效并发系统提供了底层支持。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值