11C++11多线程编程之async、future、packaged_task、promise、std::shared_future

1 async与future开启一个后台任务

1.1 概念

  • 1)async:函数模板。称为异步机制,参数为可调用对象,返回值为一个future类型的值。
  • 2)future:类模板。future对象里会存放一个值,在将来的某个时刻能够拿到。实际上是通过std::future对象的get()成员函数,它的作用为若任务未执行完毕,则阻塞等待线程执行结束,获取线程函数的返回值,否则直接可以获取返回值。
  • 3)所以开启后台任务的作用是:能让主线程和某个任务同时进行,当我们想要的结果的时候就调用get阻塞获取。可以节省时间。

1.2 async与future开启一个后台任务代码示例
1)普通函数作async的回调函数参数

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>
#include<future>

//异步后台任务
int MyPrint(int i) {
	cout << "我是异步函数模板," << " 传入参数i为:" << i << "tid=" << std::this_thread::get_id() << endl;
	return 10;
}

int main(){

	cout << "tid=" << std::this_thread::get_id() << endl;

	int i = 1;
	future<int> ret = std::async(MyPrint, i);
	//上面的异步任务并不阻塞,所以可以往下继续执行其它代码
	cout << "我是帅哥,喜欢美女" << endl;
	cout << "我是帅哥,喜欢美女" << endl;

	//若任务未执行完则阻塞获取将来值,否则可以直接获取返回值。通过future对象的get成员函数
	//cout << "通过future对象获取异步任务的返回值为:" << ret.get() << endl;
	ret.wait();//get与wait不能同时调用,否则报异常,并且每个futrue对象只能调用一次get或者一次wait,不能调两次get或者两次wait或者同时调用等。

	cout << "主线程执行!" << endl;

	return 0;
}

结果可以看到程序不会卡主在async函数中,并且创建了一个新线程,所以称为异步任务。
在这里插入图片描述
上面我们可以将future对象的get换成wait,wait函数的作用是:不需要理会返回值,只会阻塞等待异步任务结束,然后就返回。不能同时调用,否则程序报异常。并且每个futrue对象只能调用一次get或者一次wait,不能调两次get或者两次wait或者同时调用等。

2)使用类内函数作async的回调函数参数

//准备用成员函数作为线程函数的方法写线程,成为消息处理类
class A {
public:
	int MyAge(int age) {
		cout << "我是类内异步函数," << " 传入参数age为:" << age << "tid=" << std::this_thread::get_id() << endl;
		return 10;
	}

private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex;
	std::condition_variable my_cond;
};

//异步后台任务
int MyPrint(int i) {
	cout << "我是异步函数," << " 传入参数i为:" << i << "tid=" << std::this_thread::get_id() << endl;
	return 10;
}

int main(){

	cout << "tid=" << std::this_thread::get_id() << endl;
	int i = 1;
	A myobj;

	//future<int> ret = std::async(MyPrint, i);
	future<int> ret = std::async(&A::MyAge, &myobj, i);
	
	//上面的异步任务并不阻塞,所以可以往下继续执行其它代码
	cout << "我是帅哥,喜欢美女" << endl;
	cout << "我是帅哥,喜欢美女" << endl;

	//若任务未执行完则阻塞获取将来值,否则可以直接获取。通过future对象的get成员函数
	cout << "通过future对象获取异步任务的返回值为:" << ret.get() << endl;
	//ret.wait();

	cout << "主线程执行!" << endl;

	return 0;
}

结果如下:
在这里插入图片描述

3)测试主线程不写get和wait,看主线程是否仍然等待子线程执行完毕
直接给出结果(可看下图):即使不写get和wait,主线程仍然还会等待子线程结束,虽然会等待,但是最好写上,保证程序稳定。与thread的detach刚好相反,这一点需要区分一下。

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>
#include<future>

using namespace std;

//准备用成员函数作为线程函数的方法写线程,成为消息处理类
class A {
public:
	int MyAge(int age) {
		cout << "我是类内异步函数," << " 传入参数age为:" << age << "tid=" << std::this_thread::get_id() << endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(5000));//睡眠5秒测试主线程不写get和wait是否仍然等待本子线程
		return 10;
	}

private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex;
	std::condition_variable my_cond;
};

//异步后台任务
int MyPrint(int i) {
	cout << "我是异步函数," << " 传入参数i为:" << i << "tid=" << std::this_thread::get_id() << endl;
	return 10;
}

int main(){

	cout << "tid=" << std::this_thread::get_id() << endl;

	int i = 1;
	A myobj;

	//future<int> ret = std::async(MyPrint, i);
	future<int> ret = std::async(&A::MyAge, &myobj, i);
	//上面的异步任务并不阻塞,所以可以往下继续执行其它代码
	cout << "我是帅哥,喜欢美女" << endl;
	cout << "我是帅哥,喜欢美女" << endl;

	//若任务未执行完则阻塞获取将来值,否则可以直接获取。通过future对象的get成员函数
	//cout << "通过future对象获取异步任务的返回值为:" << ret.get() << endl;
	//ret.wait();

	cout << "主线程执行!" << endl;

	return 0;
}

结果看到,主线程结束了,仍然会等待子线程结束。
在这里插入图片描述

2 async函数模板详解

async函数模板详解,主要是对于其参数1的枚举宏详解。async函数的参数1为一个枚举类型,里面包含两个枚举值。

// ENUM launch
enum class launch {	// names for launch options passed to async
	async = 0x1,
	deferred = 0x2
};
  • 1)std::launch::async:异步调用,会创建新线程处理后台任务,该值为系统默认的枚举值。
  • 2)std::launch::deferred:延时调用,在future.get()或者future.wait()时才执行,并且不会创建新线程。

2.1 测试使用std::launch::async
1)

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>
#include<future>

using namespace std;

//准备用成员函数作为线程函数的方法写线程,成为消息处理类
class A {
public:
	int MyAge(int age) {
		cout << "我是类内异步函数," << " 传入参数age为:" << age << "tid=" << std::this_thread::get_id() << endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(5000));//睡眠5秒测试主线程不写get和wait是否仍然等待本子线程
		return 10;
	}

private:
	std::list<int> msgRecvQueue;//容器(消息队列),代表玩家发送过来的命令。
	std::mutex my_mutex;
	std::condition_variable my_cond;
};

//异步后台任务
int MyPrint(int i) {
	cout << "我是异步函数," << " 传入参数i为:" << i << "tid=" << std::this_thread::get_id() << endl;
	return 10;
}

int main(){

	cout << "tid=" << std::this_thread::get_id() << endl;

	int i = 1;
	A myobj;

	//future<int> ret = std::async(MyPrint, i);
	future<int> ret = std::async(std::launch::async, &A::MyAge, &myobj, i);

	//上面的异步任务并不阻塞,所以可以往下继续执行其它代码
	cout << "我是帅哥,喜欢美女" << endl;
	cout << "我是帅哥,喜欢美女" << endl;

	//若任务未执行完则阻塞获取将来值,否则可以直接获取。通过future对象的get成员函数
	cout << "通过future对象获取异步任务的返回值为:" << ret.get() << endl;
	//ret.wait();

	cout << "主线程执行!" << endl;

	return 0;
}

结果可以看到,会创建新线程启动异步任务。
在这里插入图片描述
2)当我使用std::launch::async,并且不调用get,wait时,会创建子线程并且会等待子线程结束。但是不建议缺省get,wait,避免出现可能未知的问题。
在这里插入图片描述

2.2 测试使用std::launch::deferred
1)可以看到下图,只有调用了future.get()或者future.wait()时才执行异步任务,并且没有创建新线程。
在这里插入图片描述
上述测试代码,只给出主函数,其它和上面的std::launch::async一样。

int main(){

	cout << "tid=" << std::this_thread::get_id() << endl;

	int i = 1;
	A myobj;

	//future<int> ret = std::async(MyPrint, i);
	future<int> ret = std::async(std::launch::deferred, &A::MyAge, &myobj, i);

	//上面的异步任务并不阻塞,所以可以往下继续执行其它代码
	cout << "我是帅哥,喜欢美女" << endl;
	cout << "我是帅哥,喜欢美女" << endl;

	//若任务未执行完则阻塞获取将来值,否则可以直接获取。通过future对象的get成员函数
	cout << "通过future对象获取异步任务的返回值为:" << ret.get() << endl;
	//ret.wait();

	cout << "主线程执行!" << endl;

	return 0;
}

2)可以看到,当我使用std::launch::deferred,并且不调用get,wait时,子线程根本就没有创建和执行。
在这里插入图片描述

2.3 测试同时使用std::launch::async | std::launch::deferred

  • 1)从结果可以看到,使用了std::launch::deferred,但实际并无延时调用,因为中间打印了传入参数…这些任务的内容。并且将两个宏的顺序调用传入结果也是不会延时调用。
  • 2) 这里的位或 | 真正的作用:系统可以任意选择这两个枚举宏的其中一个,看系统资源是否紧张。即系统若采用async的行为,则创建新线程并立即执行,即系统若采用deferred行为,则没有创建新线程并且延迟调用,等待get(),wait()才开始执行任务入口函数。

这些测试这个主要是为了观察,并不一定要使用到项目当中。
在这里插入图片描述

2.4 测试async函数模板不带参数
这种情况和2.3一样,都是由系统自行决定。
代码可以参考第三点3future详解的wait_for成员函数代码例子。将async参数1去掉即可。

3 future详解

future的详解实际上是成员函数的详解。

  • 1)get成员函数:若(后台)任务执行完毕,则直接获取返回值;否则阻塞等待任务结束,再获取返回值。
  • 2)wait成员函数:若(后台)任务执行完毕,则直接返回,没有获取任务返回值的能力;否则阻塞等待任务结束,也没有获取返回值的能力。这两个成员函数案例看上面代码例子即可。
  • 3)wait_for成员函数:获取异步任务的状态,可以选择休眠一定时间。看下面代码例子。
/*
	wait_for成员函数测试。
*/
#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>
#include<future>

using namespace std;

//准备用成员函数作为线程函数的方法写线程,成为消息处理类
class A {
public:
	int MyAge(int age) {
		cout << "我是类内异步函数," << " 传入参数age为:" << age << "tid=" << std::this_thread::get_id() << endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(5000));//睡眠5秒
		return 10;
	}

private:

};


int MyPrint(int i) {
	cout << "我是异步函数," << " 传入参数i为:" << i << "tid=" << std::this_thread::get_id() << endl;
	return 10;
}

int main() {

	cout << "tid=" << std::this_thread::get_id() << endl;
	int i = 1;
	A myobj;


	//1 创建异步任务
	//future<int> ret = std::async(MyPrint, i);
	future<int> ret = std::async(std::launch::async, &A::MyAge, &myobj, i);
	//上面的异步任务并不阻塞,所以可以往下继续执行其它代码
	cout << "我是帅哥,喜欢美女" << endl;

	//2 wait_for获取异步任务的状态,可以选择休眠一定时间,ret.wait_for(std::chrono::seconds(1min));支持直接写单位
	std::future_status sta = ret.wait_for(std::chrono::seconds(1));
	if (sta == std::future_status::timeout) {
		//任务未执行完毕,需要等待才能获取将来值
		cout << "任务未执行完毕,需要等待才能获取将来值,值为:" << ret.get() << endl;
	}
	else if (sta == std::future_status::ready) {
		//任务执行完毕,可以直接获取将来值
		cout << "任务执行完毕,可以直接获取将来值,值为:" << ret.get() << endl;
	}
	else if(sta == std::future_status::deferred){//延时调用,只有std::async的参数1设为std::launch::deferred这里才会执行
		//延时调用,任务只有调用get才开始执行,并等待获取将来值
		cout << "延时调用,任务开始执行,并等待获取将来值,值为:" << ret.get() << endl;
	}

	cout << "主线程执行!" << endl;

	return 0;
}

4 packaged_task详解

4.1 功能与相应传参、注意点详解

  • 1)类模板,打包任务,可以将多个可调用对象(普通函数,类成员函数,类括号重载,lambda表达式,packaged_task本身也是可调用对象)打包,方便我们一次性处理多个线程入口函数。例如我们下面的案例,将对应的入口函数映射到每一个packed_task中,然后将这些打包任务放在vector中统一处理,这样就可以处理不同的任务了,非常方便。简单说它的作用就是创建多个打包任务,处理多个不同的任务。
  • 2)包装时只需要传返回值与参数。
  • 3)在包装完回调函数之后,可以直接利用packaged_task传参数给回调函数,此时packaged_task就是一个回调函数,可以不开启线程处理。例如:
	std::packaged_task<int(int)> mypt([](int mypar) {
		cout << mypar << endl;
		return 5;
	});
	mypt(100);//可以直接调用
	future<int> ret = mypt.get_future();//同样可以使用future类对象获取返回值,future非常强大,只要该类(一般C++11以上)支持返回future对象,那么几乎可以获取任何函数返回值。例如这里的packaged_task。
	cout << ret.get() << endl;//阻塞等待获取返回值
  • 4)packaged_task不支持拷贝构造,所以直接传对象或者引用都会间接调用拷贝构造,所以只能传右值也就是移动语义,再想使用该对象时只能再次调用移动语义拿回所有权。

4.2 代码案例:
1)使用packaged_task直接调用方法执行打包任务本身。

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>
#include<future>

using namespace std;

//准备用成员函数作为线程函数的方法写线程,成为消息处理类
class A {
public:
	//可以使用类成员函数代替lambda表达式
	int MyAge(int age) {
		cout << "我是类内异步函数," << " 传入参数age为:" << age << "tid=" << std::this_thread::get_id() << endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(5000));//睡眠5秒测试主线程不写get和wait是否仍然等待本子线程
		return 10;
	}

private:
};

//可以使用普通函数代替lambda表达式
int MyPrint(int i) {
	cout << "我是异步函数," << " 传入参数i为:" << i << "tid=" << std::this_thread::get_id() << endl;
	return 10;
}

int main(){

	cout << "main" << "threadid= " << std::this_thread::get_id() << endl;

	//1 用于装载打包任务的容器
	vector < std::packaged_task<int(int)>> mytasks;//容器

	//2 打包任务对象放进容器
	std::packaged_task<int(int)> mypt([](int mypar) {
		cout << mypar << endl;
		cout << "mythread2 start" << "threadid= " << std::this_thread::get_id() << endl; //打印线程id
		std::chrono::milliseconds dura(5000); //定一个5秒的时间
		std::this_thread::sleep_for(dura);  //休息一定时常
		cout << "mythread2 end" << "threadid= " << std::this_thread::get_id() << endl; //打印线程id
		return 5;
	});

	//mytasks.push_back((mypt));//error,不支持拷贝构造
	//mytasks.push_back(std::ref(mypt));//error,同理,不支持拷贝构造,只不过传给形参时拷贝少一次,并且形参不是引用时直接报错,因为调用了拷贝构造
	mytasks.push_back(std::move(mypt));//ok,只能传右值,也就是移动语义

	//3 获取原来打包任务对象的所有权
	std::packaged_task<int(int)> mypt2;
	/*
	因为此时vec只有一个元素,所以可以这样获取到原来的元素,
	但说实话push进容器(vector,list,map)之后还是有点难再次精确获取到原来的打包对象
	*/
	auto iter = mytasks.begin();
	mypt2 = std::move(*iter); //移动语义,获取原来的任务对象的所有权
	mytasks.erase(iter);//记得要删除,上面只是改变所有权,但容器中仍然有这个元素。

	//4 开始执行调用任务
	//方法1:直接调用,相当于函数调用
	mypt2(105);
	//方法2:线程调用,相当于后台任务.注意:packaged_task本身也是一个可调用对象,
	//可以直接传参(由于不支持拷贝构造,所以需要传引用,最好别传移动,因为这样的话本线程就无法再拿到所有权
	//thread th(std::ref(mypt2), 110);
	//th.join();

	//5 获取打包任务的返回值
	std::future<int> result = mypt2.get_future();
	cout << result.get() << endl;

	cout << "主线程执行!" << endl;

	return 0;
}

结果如下,当调用打包任务,程序就会去执行对应的回调函数,非常方便。
在这里插入图片描述

2)使用packaged_task作为可调用对象给线程传参,以此执行packaged_task打包任务。
将上面的代码thread th开启即可。
结果可以看到,程序正常运行。
在这里插入图片描述

3)额外添加打包普通函数的写法

int MyPrint(int i) {
	cout << "我是异步函数," << " 传入参数i为:" << i << "tid=" << std::this_thread::get_id() << endl;
	return 10;
}

std::packaged_task<int(int)> mypt3(MyPrint);
mypt3(300);
std::future<int> res = mypt3.get_future();
cout << res.get() << endl;

5 std::promise

  • 1)类模板,可以通过set_value保存一个值,在将来的某个时刻通过配合future使用。
  • 2)主要用于不同线程函数中传递结果,一般是一个线程运行结束已经获取到返回值的情况下,再通过promise保存的furure对象传给另一个线程使用。
  • 3)当然,async与future也可以在线程间传递结果,并非只是promise与future,反正将来值离不开future。

简单代码案例:

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>
#include<future>

using namespace std;

//注意第一个形参,用于不同线程中传递结果
void mythread(std::promise<int>&tmpp, int calc){
	//做一系列复杂的操作
	calc++;
	calc *= 10;

	//做其他运算,比如整整花费了5秒钟
	std::chrono::milliseconds dura(5000); //定一个5秒的时间
	std::this_thread::sleep_for(dura);  //休息一定时常

	int result = calc; //保存结果
	tmpp.set_value(result);  //结果保存到了tmpp这个对象中
}

void mythread2(std::future<int> &tmpf){
	auto result = tmpf.get();//移动语义,此时tmp的实参已经没有所有权,所以必须注意get的使用(std::shared_future的get可以解决,因其不是移动语义)
	cout << "mythread result = " << result << endl;
}

int main(){

	std::promise<int> myprom; //声明一个std::promise对象myprom,保存的值类型为int;
	std::thread t1(mythread, std::ref(myprom), 180);
	t1.join();

	//promise与future绑定,用于获取线程返回值
	std::future<int> fu1 = myprom.get_future();

	//将上面线程的结果作为另一个线程中的开始
	std::thread t2(mythread2, std::ref(fu1));
	t2.join(); //等mythread2执行完毕

	//cout << fu1.get() << endl;//error,不能再get,因为fu1在mythread2已经被移动语义夺走所有权,这一点需要注意get的使用,可s只有共享future代替
	cout << "I love China!" << endl;

	return 0;
}

6 std::shared_future

  • 1)解决future只能调用一次get的弊端,因为future的get是一个移动语义,当形参传引用时,一旦在某个线程被调用了一次get,那么主线程或者其它线程就不能再使用,代码可以看上面promise的注释。

代码案例:

#include<iostream>
#include<thread>
#include<string>
#include<vector>
#include<list>
#include<mutex>
#include<future>

using namespace std;

//注意第一个形参,用于不同线程中传递结果
void mythread(std::promise<int>&tmpp, int calc){
	//做一系列复杂的操作
	calc++;
	calc *= 10;

	//做其他运算,比如整整花费了5秒钟
	std::chrono::milliseconds dura(5000); //定一个5秒的时间
	std::this_thread::sleep_for(dura);  //休息一定时常

	int result = calc; //保存结果
	tmpp.set_value(result);  //结果保存到了tmpp这个对象中
}

//void mythread2(std::future<int> &tmp){
//	auto result = tmp.get();//移动语义,此时tmp的实参已经没有所有权,所以必须注意get的使用(std::shared_future的get可以解决,因其不是移动语义)
//	cout << "mythread result = " << result << endl;
//}

void mythread2(std::shared_future<int> &tmp) {
	auto result = tmp.get();//拷贝语义,主线程仍能使用
	cout << "mythread result = " << result << endl;
}

int main(){

	//1 声明一个std::promise对象myprom,保存的值类型为int;
	std::promise<int> myprom; 
	std::thread t1(mythread, std::ref(myprom), 180);
	t1.join();

	//2 promise与future绑定,用于获取线程返回值
	std::future<int> fu1 = myprom.get_future();

	//3 使用共享的shared_future,方便多个线程来回切换使用get
	std::shared_future<int> sful;
	if (fu1.valid()) {
		//有效才移动
		sful = std::move(fu1);
		//sful = fu1.share();//和移动语义一样
		//也可以直接在上面获取get_future的时候使用shared_future接收
		//即std::shared_future<int> sful = myprom.get_future();
	}
	
	//4 将上面线程的shared_future结果作为另一个线程中的开始
	std::thread t2(mythread2, std::ref(sful));
	t2.join(); //等mythread2执行完毕

	//5 只有传shared_future才能在不同线程多次get
	//cout << fu1.get() << endl;//error,不能在get,因为fu1在mythread2已经被移动语义夺走所有权,这一点需要注意get的使用
	cout << sful.get() << endl;//ok


	cout << "I love China!"  << endl;
	return 0;
}

7 总结

  • 学习的这些东西,到底怎么用,什么时候用?
    我们学习这些东西的目的,并不是要把它们都用在咱们自己的实际开发中。
    相反,如果我们能够用最少的东西能够写出一个稳定、高效的多线程程序,更值得赞赏。
    为了成长,必须阅读一些高手写的代码,从而快速实现自己代码的积累;我们的技术就会有一个大幅度的提升;
    更愿意将学习这些内容的理由解释为:为我们将来能够读懂高手甚至大师写的代码铺路!——
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值