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