1.4 异步操作
- std::future : 异步指向某个任务,然后通过future特性去获取任务函数的返回结果。
- std::aysnc: 异步运行某个任务函数
- std::packaged_task :将任务和feature绑定在一起的模板,是一种封装对任务的封装。
- std::promise
1.4.1 std::aysnc和std::future
std::future用于在多线程编程中处理异步操作的结果。它提供了一种机制来获取异步任务的返回值或异常,并在需要时等待异步任务完成。
线程可以周期性的在这个future上等待一小段时间,检查future是否已经ready,如果没有,该线程可以先去做另一个任务。一旦future就绪,该future就无法复位(无法再次使用这个future等待这个事件),所以future代表的是一次性事件。
future的类型
在库的头文件中声明了两种future,唯一future(std::future)和共享future(std::shared_future)。
这两个是参照std::unique_ptr和std::shared_ptr设立的,前者的实例是仅有的一个指向其关联事件的实例。而后者可以有多个实例指向同一个关联事件,当事件就绪时,所有指向同一事件的std::shared_future实例会变成就绪
future的使用
std::future是一个模板,例如std::future,模板参数就是期待返回的类型,虽然future被用于线程间通信,但其本身却并不提供同步访问。
future使用的时机是当你不需要立刻得到一个结果的时候,你可以开启一个线程帮你去做一项任务,并期待这个任务的返回。但是std::thread并没有提供这样的机制,这就需要用到std::async和std::future
std::async返回一个std::future对象,而不是给你一个确定的值。当你需要使用这个值的时候,对future使用get(),线程就会阻塞直到future就绪,然后返回该值.
#include <iostream> #include <future> #include <thread> using namespace std; int find_result_to_add() { std::this_thread::sleep_for(std::chrono::seconds(5)); // 用来测试异步延迟的影响 std::cout << "find_result_to_add" << std::endl; return 1 + 1; } int find_result_to_add2(int a, int b) { std::this_thread::sleep_for(std::chrono::seconds(5)); // 用来测试异步延迟的影响 return a + b; } void do_other_things() { std::cout << "do_other_things" << std::endl; } int main() { std::future<int> result = std::async(find_result_to_add); // async把find_result_to_add交给其他线程异步运行,不会阻塞do_other_things();的运行 // 另外两种声明方式,推荐第三中 // std::future<decltype (find_result_to_add())> result = std::async(find_result_to_add); // decltype自动推导参数类型 // auto result = std::async(find_result_to_add); // 推荐的写法 do_other_things(); std::cout << "result: " << result.get() << std::endl; // get会阻塞等待任务函数的返回值 // std::future<decltype(find_result_to_add2(int, int))> result2 = std::async(find_result_to_add2, 10, 20); //错误 std::future<decltype (find_result_to_add2(0, 0))> result2 = std::async(find_result_to_add2, 10, 20); std::cout << "result2: " << result2.get() << std::endl; // 延迟是否有影响? return 0; }
上面代码中,std::async(find_result_to_add)把函数find_result_to_add异步运行,不会阻塞主线程。当需要返回值时候,再通过result.get()返回。
1.4.2 std::packaged_task
std::packaged_task用于将可调用对象(如函数、Lambda 表达式)封装成一个具备异步执行能力的任务对象。它可以帮助我们将任务的执行与结果的获取分离开来,以便在需要时获取任务的返回值。
使用 std::packaged_task 将可调用对象封装成一个任务对象 task。通过调用 get_future() 方法,我们获取了与任务关联的std::future对象 result,用于获取任务的返回值。
#include <iostream> #include <future> #include <thread> using namespace std; int add(int a, int b, int c) { std::cout << "call add\n"; return a + b + c; } void do_other_things() { std::cout << "do_other_things" << std::endl; } int main() { std::packaged_task<int(int, int, int)> task(add); // 1. 封装任务,还没有运行,意味着不会阻塞 do_other_things(); std::future<int> result = task.get_future(); // 2.获取 future,注意这里也不运行 task(1, 1, 2); //3.这里才真正运行。并且必须要让任务执行,否则在get()获取future的值时会一直阻塞 std::cout << "result:" << result.get() << std::endl; return 0; }
1.4.3 std::promise
用于在一个线程中产生某个值,并在另一个线程中获取该值。它提供了一种线程之间传递数据的方式,通过 std::promise 和 std::future 的结合使用,可以实现线程间的数据传递和同步等功能。
#include <future> #include <string> #include <thread> #include <iostream> using namespace std; void print1(std::promise<std::string>& p) { std::cout << "print1 sleep" << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); p.set_value("set string"); // 履行承诺,设置返回值 } void do_some_other_things() { std::cout << "do_some_other_things" << std::endl; } int main() { std::cout << "main1 -------------" << std::endl; // 1.创建了一个 std::promise 对象 promise,用于承诺将计算结果传递给后续处理。 std::promise<std::string> promise; // 注意类型: // 2.调用 get_future() 方法,我们获取了与承诺关联的 std::future 对象 result,用于获取计算结果。 std::future<std::string> result = promise.get_future(); // future // 3.创建了一个新线程 worker,并将承诺对象 promise 传递给 print1 函数 std::thread t(print1, std::ref(promise)); // 线程设置 传引用std::ref do_some_other_things(); // 4.等待子线程完成 t.join(); std::cout << "wait get result" << std::endl; // 5.在其他操作完成后,我们使用 get() 函数从 result 中获取计算结果。 // 如果结果还没有被设置(即承诺还没有被履行),调用 get() 会导致当前线程阻塞,直到结果可用为止。 // 一旦结果可用,我们就可以使用该结果进行后续处理。 std::cout <<"result " << result.get() << std::endl; // 在主线程等待 promise的返回 result set string return 0; }
main1 ------------- do_some_other_things print1 sleep wait get result result set string
二、function和bind用法
std::function 和 std::bind 都是 C++ 的函数工具,用于实现函数对象的封装和绑定。
2.1 function
std::function 是一个通用的函数封装类模板,用于存储、管理和调用可调用对象(如函数、函数指针、Lambda 表达式等)。它提供了一种统一的方式来处理不同类型的可调用对象,并且可以像普通函数一样进行调用。类似C语言的函数指针。
包含头文件:#include <functional>
1)保存普通函数
void func1(int a) { cout << a << endl; } std::function<void(int a)> func; func = func1; func(2); //2
2)保存lambda表达式
std::function<void()> func_1 = [](){cout << "hello world" << endl;}; func_1(); //hello world
3)保存成员函数
class A{ public: A(string name) : name_(name){} void func3(int i) const {cout <<name_ << ", " << i << endl;} private: string name_; }; //3 保存成员函数 std::function<void(const A&,int)> func3_ = &A::func3; A a("darren"); func3_(a, 1);
2.2 bind
用于将可调用对象与其参数进行绑定,生成一个新的可调用对象。
调用bind的一般形式:
auto newCallable = bind(callable, arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。即,当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:1为newCallable的第一个参数,_2为第二个参数,以此类推。
范例
#include <iostream> #include <functional> using namespace std; class A { public: // 重载fun_3,主要bind的时候需要 // std::bind((void(A::*)(int, int))&A::fun_3 void fun_3(int k,int m) { cout << "fun_3 a = " << a<< endl; cout<<"print: k="<<k<<",m="<<m<<endl; } // std::bind((void(A::*)(string))&A::fun_3 void fun_3(string str) { cout<<"print: str="<<str<<endl; } int a; }; void fun_1(int x,int y,int z) { cout<<"fun_1 print: x=" <<x<<",y="<< y << ",z=" <<z<<endl; } void fun_2(int &a,int &b) { a++; b++; cout<<"print: a=" <<a<<",b="<<b<<endl; } void func2_1(int a, int b) { cout << "func2_1 a + b = " << a+b << endl; } int func2_1(string a, string b) { cout << "func2_1 a + b = " << a << b<< endl; return 0; }
1)先看绑定函数
//f1的类型为 function<void(int, int, int)> cout << "\n\nstd::bind(fun_1, 1, 2, 3) -----------------\n"; auto f1 = std::bind(fun_1, 1, 2, 3); //表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3 f1(); //print: x=1,y=2,z=3 cout << "\n\nstd::bind(fun_1, 10, 20, 30) -----------------\n"; auto f11 = std::bind(fun_1, 10, 20, 30); //表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3 f11(); cout << "\n\nstd::bind(fun_1, placeholders::_1,placeholders::_2, 3) -----------------\n"; auto f2 = std::bind(fun_1, placeholders::_1, placeholders::_2, 3); //表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f2 的第一,二个参数指定 f2(1,2);//print: x=1,y=2,z=3 f2(10,21,30); // 传入30也没有用 cout << "\n\nstd::bind(fun_1,placeholders::_2,placeholders::_1,3) -----------------\n"; auto f3 = std::bind(fun_1,placeholders::_2,placeholders::_1,3); //表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别由调用 f3 的第二,一个参数指定 //注意: f2 和 f3 的区别。 f3(1,2);//print: x=2,y=1,z=3 cout << "\n\nstd::bind(fun_2, placeholders::_1, n) -----------------\n"; int m = 2; int n = 3; auto f4 = std::bind(fun_2, placeholders::_1, n); //表示绑定fun_2的第一个参数为n, fun_2的第二个参数由调用f4的第一个参数(_1)指定。 f4(m); //print: a=3,b=4 cout<<"m="<<m<<endl;//m=3 说明:bind对于不事先绑定的参数,通过std::placeholders传递的参数是通过引用传递的,如m cout<<"n="<<n<<endl;//n=3 说明:bind对于预先绑定的函数参数是通过值传递的,如n
std::bind(fun_1, 1, 2, 3) ----------------- fun_1 print: x=1,y=2,z=3 std::bind(fun_1, 10, 20, 30) ----------------- fun_1 print: x=10,y=20,z=30 std::bind(fun_1, placeholders::_1,placeholders::_2, 3) ----------------- fun_1 print: x=1,y=2,z=3 fun_1 print: x=10,y=21,z=3 std::bind(fun_1,placeholders::_2,placeholders::_1,3) ----------------- fun_1 print: x=2,y=1,z=3 std::bind(fun_2, placeholders::_1, n) ----------------- print: a=3,b=4 m=3 n=3
2)再看一下绑定类
cout << "\n\nstd::bind(&A::fun_3, &a,placeholders::_1,placeholders::_2) -----------------\n"; A a; a.a = 10; //f5的类型为 function<void(int, int)>, 使用auto关键字 auto f5 = std::bind((void(A::*)(int, int))&A::fun_3, &a, 40, 50); //void是返回类型,(A::*) 是指向类 A 的成员的作用域限定符,(int, int) 是函数参数列表 f5(10,20); cout << "\n\nstd::bind(&A::fun_3, &a2,placeholders::_1,placeholders::_2) -----------------\n"; A a2; a2.a = 20; //f5的类型为 function<void(int, int)> auto f6 = std::bind((void(A::*)(int, int))&A::fun_3, &a2,placeholders::_1,placeholders::_2); //使用auto关键字 f6(10,20);// cout << "\n\nstd::bind(&A::fun_3, a,std::placeholders::_1,std::placeholders::_2) -----------------\n"; auto f_str = std::bind((void(A::*)(string))&A::fun_3, a,std::placeholders::_1); f_str("darren");
std::bind(&A::fun_3, &a,placeholders::_1,placeholders::_2) ----------------- fun_3 a = 10 print: k=40,m=50 std::bind(&A::fun_3, &a2,placeholders::_1,placeholders::_2) ----------------- fun_3 a = 20 print: k=10,m=20 std::bind(&A::fun_3, a,std::placeholders::_1,std::placeholders::_2) ----------------- print: str=darren
说明的一点是,如果没有重载的话,可以直接写
auto f5 = std::bind(&A::fun_3, &a, 40, 50);
三、可变参数模板
3.1 语法
可变参数模板是 C++11 提供的一种特性,用于处理数量可变的参数类型。通过可变参数模板,可以在一个模板函数或类中接受任意数量和类型的参数。
template<class... T, class... Args> void functionName(T first, Args... args) { // 函数体 }
1)使用 template 关键字定义一个模板函数,其模板类型参数由 class... T表示。T 是第一个参数的类型,而 Args 是剩余参数的类型包(parameter pack)。
2)第一个参数 T first 是必传的,用来接收第一个参数。
3)Args... args 是一个参数包扩展(pack expansion),用于接收剩余的参数。通过 … 表示参数包的展开,可以将其中的每个参数依次进行处理。
例如
#include <iostream> using namespace std; template <class... T> void fun(T... args) { cout << sizeof...(args) << endl; //打印变参的个数 } int main() { fun(); //0 fun(1, 2); //2 fun(1, 2.5, ""); //3 return 0; }
这个例子只是简单的将可变模版参数的个数打印出来,如果我们需要将参数包中的每个参数打印出来的话就需要通过一些方法了。展开可变模版参数函数的方法一般有三种:
1)通过递归函数来展开参数包,
2)是通过逗号表达式来展开参数包。
3)c++ 17的折叠表达式
3.2 递归函数来展开参数包
通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数,递归终止函数正是用来终止递归的
#include <iostream> using namespace std; //递归终止函数 void print() { cout << "empty" << endl; } //展开函数 template <class T, class ...Args> void print(T head, Args... rest) { cout << "parameter " << head << endl; print(rest...); } int main(void) { print(1,2,3,"zxm", "xxjp"); return 0; }
parameter 1 parameter 2 parameter 3 parameter zxm parameter xxjp empty
上例会输出每一个参数,直到为空时输出empty。展开参数包的函数有两个,一个是递归函数,另外一个是递归终止函数,参数包Args...在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当没有参数时,则调用非模板函数print终止递归过程。
上面的递归终止函数还可以写成这样:
template <class T> void print(T t) { cout << t << endl; }
3.3 逗号表达式展开参数包
递归函数展开参数包是一种标准做法,也比较好理解,但就是必须要一个重载的递归终止函数,即必须要有一个同名的终止函数来终止递归。
逗号表达式是 C++ 中的一种表达式,由逗号(,)操作符连接多个子表达式而形成。逗号表达式的求值顺序是从左到右,每个子表达式都会被依次求值,最终整个逗号表达式的结果是最后一个子表达式的值。
int a = 1, b = 2, c; c = (a++, b++, a + b); // 逗号表达式 (a++, b++) 的结果是 b,c 的值为 a + b 的结果
初始化列表(initializer list)是一种在 C++ 中用于初始化数组、结构体、类等复合类型对象的语法。它使用花括号 {} 将初始化的值括起来,多个值之间用逗号分隔。
int arr[] = {1, 2, 3, 4, 5}; // 初始化整型数组
逗号表达式可以不通过递归方式来展开参数包,这种方式需要借助逗号表达式和初始化列表。比如前面print的例子可以改成这样:
#include <iostream> using namespace std; template <class T> void printarg(T t) { cout << t << endl; } template <class ...Args> void expand(Args... args) { int arr[] = {(printarg(args), 0)...}; } int main() { expand(1,2,3,"zxm"); return 0; }
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。
expand函数中的逗号表达式:(printarg(args), 0),先执行printarg(args),再得到逗号表达式的结果0。
同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组,{(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。
由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
我们可以把上面的例子再进一步改进一下,将函数作为参数,就可以支持lambda表达式了,从而可以少写一个递归终止函数了,具体代码如下:
#include <iostream> using namespace std; template<class F, class... Args>void expand(const F& f, Args&&...args) { //这里用到了完美转发 initializer_list<int>{(f(std::forward< Args>(args)),0)...}; } int main() { expand([](int i){cout<<i<<endl;}, 1,2,3); return 0; }
3.4 折叠表达式
#include <iostream> template<typename... Args> void printArgs(Args... args) { ((std::cout << args << std::endl), ...); // 折叠表达式展开参数包并换行 // ((std::cout << args << ' '), ...); // 折叠表达式展开参数包并添加空格 } int main() { printArgs(1, 2.5, "hello", 'a'); return 0; }
需要注意的是,编译要加上-std = c++17