😉一、C++历史版本编年史
年份 | C++标准 | 通用名 |
1978 | C with Classes | - |
1998 | ISO/IEC 14882:1998 | C++98 |
2003 | ISO/IEC 14882:2003 | C++03 |
2011 | ISO/IEC 14882:2011 | C++11 |
2014 | ISO/IEC 14882:2014 | C++14 |
2017 | ISO/IEC 14882:2017 | C++17 |
2020 | ISO/IEC 14882:2020 | C++20 |
2023 | waiting | waiting |
😆小知识
什么是C++0X:上一个版本的C++国际标准是2003年发布的,所以叫C++ 03。然后C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是07年发布,所以最初这个标准叫C++ 07。但是到06年的时候,官方觉得07年肯定完不成C++ 07,而且官方觉得08年可能也完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。
😆小知识
C++的简单由来:
1998年是C++标准委员会成立的第一年,以后每5年视实际需要更新一次标准。
2009年,C++标准有了一次更新,一般称该草案为C++0x。
C++0x是C++11标准成为正式标准之前的草案临时名字。
后来,2011年,C++新标准标准正式通过,更名为ISO/IEC 14882:2011,简称C++11。
😆小知识
C++11后的进化史:
- C++11,先前被称作C++0x,即ISO/IEC 14882:2011,是C++编程语言的一个标准。
它取代第二版标准ISO/IEC 14882:2003 (第一版ISO/IEC 14882:1998公开于1998年, 第二版于2003年更新,分别通称C++98以及C++03,两者差异很小),且已被C++14取代- C++14 旨在作为C++11的一个小扩展,主要提供漏洞修复和小的改进。
2014年8月18日,经过C++标准委员投票,C++14标准获得一致通过。ISO/IEC 14882:2014- C++17 又称C++1z,是继 C++14 之后,C++ 编程语言 ISO/IEC 标准的下一次修订的非正式名称。
官方名称 ISO/IEC 14882:2017,基于 C++ 11,C++ 17 旨在简化该语言的日常使用,使开发者可以更简单地编写和维护代码。
-C++20在2020年更新,用C++之父的话说C++20目的就是stability–稳定(稳定压到一切)和
evolution–更新(与时俱进,走到潮流之巅)
🎂二、C++11新特性
auto自动类型推导
- auto:让编译器在编译器就推导出变量的类型,可以通过=右边的类型推导出变量的类型,类似于C#之中的var。可以用它来躲开一堆长长的类型名,比如STL容器的iterator。
- auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。而是否会造成编译期的时间消耗,我认为是不会的,在未使用auto时,编译器也需要得知右操作数的类型,再与左操作数的类型进行比较,检查是否可以发生相应的转化,是否需要进行隐式类型转换。
- 使用auto会让你的代码变得容易维护,上边例子中,for循环里所有变量的类型都是由b推导出来了。如果有人突然觉得这里int太小了,要改为unsigned long呢?如果你像上边那样写,你需要做的事情无非就是把int改为unsigned long——其他变量的类型变化就交给编译器和auto来处理好了i会自动变为unsigned long类型。
#include "iostream" #include "vector" auto func() { //函数的返回值可以用auto自动推导 auto b = std::vector<int>{1, 2, 3, 4}; for (auto i: b) {//auto还可以这么用 std::cout << i << std::endl; } } int main() { auto a = 10;//无需显式的声明int a=10,编译器在运行的时候会自动推导。 return 0; };
注意事项:
- auto 变量必须在定义时初始化,这类似于const关键字。
int main() { auto a=0; auto b;//不进行初始化会报错 };
- 定义在一个auto序列的变量必须始终推导成同一类型。
int main() { auto a=10,b=10.0;//一个是int一个是非int类型,无法正确推导 };
- 如果初始化表达式是引用,则去除引用语义。
#include "iostream" #include "vector" int main() { int a = 10; int &b = a; auto c = b; auto &d = b; c = 100; std::cout << a << std::endl;//输出为100 d = 1000; std::cout << a << std::endl;//输出为1000 std::cout << typeid(c).name() << std::endl;//输出为int std::cout << typeid(d).name() << std::endl;//输出为int std::cout << &a << std::endl;//输出为0000003225CFFCB4 std::cout << &b << std::endl;//输出为0000003225CFFCB4 std::cout << &c << std::endl;//输出为0000003225CFFCD4 std::cout << &d << std::endl;//输出为0000003225CFFCB4 };
- 如果初始化表达式为const或volatile(或者两者兼有),则除去const/volatile语义。
#include "iostream" #include "vector" int main() { const int a1 = 10; auto b1= a1; //b1的类型为int而非const int(去除const) const auto c1 = a1;//此时c1的类型为const int b1 = 100;//合法 c1 = 100;//非法 };
- 如果auto关键字带上&号,则不去除const语意。
#include "iostream" #include "vector" int main() { const int a2 = 10; auto &b2 = a2;//因为auto带上&,故不去除const,b2类型为const int std::cout<< typeid(a2).name()<<std::endl;//int 为const int std::cout<< typeid(b2).name()<<std::endl;//int 为const int b2=1;//非法 };
- 初始化表达式为数组时,auto关键字推导类型为指针。若表达式为数组且auto带上&,则推导类型为数组类型。
#include "iostream" #include "vector" int main() { int a[3] = {1, 2, 3}; auto b = a; auto &c = a; std::cout << typeid(a).name() << std::endl;//int [3] std::cout << typeid(b).name() << std::endl;//int *__ptr64 std::cout << typeid(c).name() << std::endl;//int [3] };
- 函数或者模板参数不能被声明为auto
void func(auto a) //错误 { //... }
- 时刻要注意auto并不是一个真正的类型。
auto仅仅是一个占位符,它并不是一个真正的类型,不能使用一些以类型为操作数的操作符,如sizeof或者typeid。
cout << sizeof(auto) << endl;//错误 cout << typeid(auto).name() << endl;//错误
decltype
decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算,decltype不会像auto一样忽略引用和cv属性,decltype会保留表达式的引用和cv属性
对于decltype(exp)有
- exp是表达式,decltype(exp)和exp类型相同
- exp是函数调用,decltype(exp)和函数返回值类型相同
- 其它情况,若exp是左值,decltype(exp)是exp类型的左值引用
#include "iostream" #include "vector" #include "functional" void *fun() { return nullptr; } int main() { int a = 3; decltype(a) b = 0; decltype(fun()) c;// decltype(a + b) d = 0;//d是int,因为(a+b)返回一个右值 decltype(a += b) e = d;//e是int&,因为(a+=b)返回一个左值 std::cout << typeid(a).name() << std::endl;//int std::cout << typeid(b).name() << std::endl;//int std::cout << typeid(c).name() << std::endl;//void * __ptr64 std::cout << typeid(d).name() << std::endl;//int std::cout << typeid(e).name() << std::endl;//int std::cout << d << std::endl;//0 e = 5; std::cout << d << std::endl;//5 因为e是int& };
😆小知识
decltype如何配合auto使用
auto和decltype一般配合使用在推导函数返回值的类型问题上,
例如
template decltype(t + u) add(T t, U u) { // t和u尚未定义 return t + u; }
这段代码在C++11上是编译不过的,因为在decltype(t +u)推导时,t和u尚未定义,就会编译出错,所以有了下面的叫做返回类型后置的配合使用方法:
template auto add(T t, U u) -> decltype(t + u) { return t + u; }
返回值后置类型语法就是为了解决函数返回制类型依赖于参数但却难以确定返回值类型的问题。
编译器自动判断了返回值类型,牛啊!
左值右值
这个知识点请跳转我另一篇文章,讲的非常的清楚明白
😚2023-3-9-一篇简短的文章把C++左右值关系讲的透透彻彻
列表初始化
C++11新增了列表初始化的概念。
在C++11中可以直接在变量名后面加上初始化列表来进行对象的初始化。
#include "iostream" #include "vector" #include "functional" int main() { std::vector a {1, 2, 3, 4, 5, 6}; };
列表初始化的一些规则:
首先说下聚合类型可以进行直接列表初始化,这里需要了解什么是聚合类型:
- 类型是一个普通数组,如int[5],char[],double[]等
- 类型是一个类,且满足以下条件:
没有用户声明的构造函数
没有用户提供的构造函数(允许显示预置或弃置的构造函数)
没有私有或保护的非静态数据成员
没有基类
没有虚函数
没有{}和=直接初始化的非静态数据成员
没有默认成员初始化器
#include "iostream" #include "vector" #include "functional" class A{ public: int a;//如果为private则错误 }; int main() { A a{0}; };
struct A { int a; int b; int c; A(int, int){} }; int main() { A a{1, 2, 3};// error,A有自定义的构造函数,不能列表初始化 }
struct A { int a; int b; virtual void func() {} // 含有虚函数,不是聚合类 }; struct Base {}; struct B : public Base { // 有基类,不是聚合类 int a; int b; }; struct C { int a; int b = 10; // 有等号初始化,不是聚合类 }; struct D { int a; int b; private: int c; // 含有私有的非静态数据成员,不是聚合类 }; struct E { int a; int b; E() : a(0), b(0) {} // 含有默认成员初始化器,不是聚合类 };
上面列举了一些不是聚合类的例子,对于一个聚合类型,使用列表初始化相当于对其中的每个元素分别赋值;对于非聚合类型,需要先自定义一个对应的构造函数,此时列表初始化将调用相应的构造函数。
😆小知识
我们平时开发使用STL过程中可能发现它的初始化列表可以是任意长度,大家有没有想过它是怎么实现的呢,答案是std::initializer_list,看下面这段示例代码:
struct CustomVec { std::vector data; CustomVec(std::initializer_list list) { for (auto iter = list.begin(); iter != list.end(); ++iter) { data.push_back(*iter); } } };
std::initializer_list,它可以接收任意长度的初始化列表,但是里面必须是相同类型T,或者都可以转换为T。
std::function
std::function是可调用对象的封装器,可以把std::function看做一个函数对象,用于表示函数这个抽象概念。std::function的实例可以存储、复制和调用任何可调用对象,存储的可调用对象称为std::function的目标,若std::function不含目标,则称它为空,调用空的std::function的目标会抛出std::bad_function_call异常。
😆小知识
什么是可调用对象
满足以下条件之一就可称为可调用对象:
是一个函数指针
是一个具有operator()成员函数的类对象(传说中的仿函数),lambda表达式
是一个可被转换为函数指针的类对象
是一个类成员(函数)指针
bind表达式或其它函数对象
#include "iostream" #include "vector" #include "functional" class A { public: A(int a) {} void print_a() { std::cout << a << std::endl; } public: int a = 4; }; void print_num(int i) { std::cout << i << std::endl; } int main() { //存储自由函数 std::function<void(int)> function = print_num; function(1);//输出1 //存储lambda std::function<void(int)> function1 = [](int i) { std::cout << i << std::endl; }; function1(2);//输出2 //存储到 std::bind 调用的结果 std::function < void() > function2 = std::bind(print_num, 3); function2();//输出3 如果function2为void(int)类型,那么该函数传的参数不会影响结果,实际的参数是bind绑定的参数 //存储到成员函数的调用 std::function<void(A)> function3 = &A::print_a; function3(A{4});//输出4 };
当给std::function填入合适的参数表和返回值后,它就变成了可以容纳所有这一类调用方式的函数封装器。std::function还可以用作回调函数,或者在C++里如果需要使用回调那就一定要使用std::function,特别方便。
std::bind
使用std::bind可以将可调用对象和参数一起绑定,最主要的功能就是实现延迟调用
- 将可调用对象与参数一起绑定为另一个std::function供调用
- 将n元可调用对象转成m(m < n)元可调用对象,绑定一部分参数,这里需要使用std::placeholders
#include "iostream" #include "vector" #include "functional" #include <memory> void print_num(int i,int k,int j) { std::cout << i << std::endl; } int main() { //存储自由函数 std::function<void(int,int)> function = std::bind(print_num,5,std::placeholders::_1,std::placeholders::_2); function(2,3);//输出1 };
lambda表达式
lambda表达式可以说是c++11引用的最重要的特性之一,它定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用,一般有如下语法形式:
auto func = [capture] (params) opt -> ret { func_body; };
其中func是可以当作lambda表达式的名字,作为一个函数使用,capture是捕获列表,params是参数表,opt是函数选项(mutable之类), ret是返回值类型,func_body是函数体。
一个完整的lambda表达式:
auto func1 = [](int a) -> int { return a + 1; }; auto func2 = [](int a) { return a + 2; }; cout << func1(1) << " " << func2(2) << endl;
如上代码,很多时候lambda表达式返回值是很明显的,c++11允许省略表达式的返回值定义。
lambda表达式允许捕获一定范围内的变量:
- []不捕获任何变量
- [&]引用捕获,捕获外部作用域所有变量,在函数体内当作引用使用
- [=]值捕获,捕获外部作用域所有变量,在函数内内有个副本使用
- [=, &a]值捕获外部作用域所有变量,按引用捕获a变量
- [a]只值捕获a变量,不捕获其它变量
- [this]捕获当前类中的this指针
#include "iostream" #include "vector" #include "functional" int main() { //值捕获 int a = 0; std::function < void() > function = [=]() -> void { a=10;//报错 std::cout << a << std::endl;//0 }; function(); std::cout << a << std::endl;//0 //引用捕获 std::function < void() > function1 = [&]() -> void { a=10; std::cout << a << std::endl;//10 }; function1(); std::cout << a << std::endl;//10 };
代码中的a=10是编译不过的,因为我们修改了按值捕获的外部变量,其实lambda表达式就相当于是一个仿函数,仿函数是一个有operator()成员函数的类对象,这个operator()默认是const的,所以不能修改成员变量,而加了mutable,就是去掉const属性。
#include "iostream" #include "vector" #include "functional" int main() { //值捕获加mutable int a = 0; std::function < void() > function = [=]() mutable -> void { a=10;//此时不报错 std::cout << a << std::endl;//10 }; function(); std::cout << a << std::endl;//0 };
模板的改进
- 模板的右尖括号
C++11之前是不允许两个右尖括号出现的,会被认为是右移操作符,所以需要中间加个空格进行分割,避免发生编译错误。 - 模板的别名
C++11引入了using,可以轻松的定义别名,而不是使用繁琐的typedef。
typedef std::vector<std::vector<int>> vvi; // before c++11 using vvi = std::vector<std::vector<int>>; // c++11
- 函数模板的默认模板参数
C++11之前只有类模板支持默认模板参数,函数模板是不支持默认模板参数的,C++11后都支持。
并发
c++11引入了std::thread来创建线程,支持对线程join或者detach
#include "iostream" #include "thread" #include "memory" #include "functional" int main() { std::function < void() > func = []() { for (int i = 0; i < 10; i++) { std::cout << i << " "; } std::cout << std::endl; }; std::thread t(func); if(t.joinable()) { t.detach();//使用detach()函数会让线程在后台运行,即说明主线程不会等待子线程运行结束才结束通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,没有任何显式的用户接口,并在后台运行的线程。这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。另一方面,分离线程的另一方面只能确定线程什么时候结束,发后即忘(fire andforget)的任务就使用到线程的这种方式 } std::function < void(int) > func1 = [](int k) { for (int i = 0; i < k; i++) { std::cout << i << " "; } std::cout << std::endl; }; std::thread tt(func1, 20); if(tt.joinable()) { tt.join();//join()函数是一个等待线程完成函数,主线程需要等待子线程运行结束了才可以结束 } return 0; }
std::mutex是一种线程同步的手段,用于保存多线程同时操作的共享数据。
mutex分为四种:
- std::mutex:独占的互斥量,不能递归使用,不带超时功能
- std::recursive_mutex:递归互斥量,可重入,不带超时功能
- std::timed_mutex:带超时的互斥量,不能递归
- std::recursive_timed_mutex:带超时的互斥量,可以递归使用
#include "iostream" #include "thread" #include "mutex" #include "functional" std::timed_mutex mutex; int main() { std::function < void(int) > func1 = [](int k) { mutex.try_lock_for(std::chrono::microseconds (1)); for (int i = 0; i < k; i++) { std::cout << i << " "; } std::cout << std::endl; mutex.unlock(); }; std::vector<std::thread> threads; for(int i=0;i<5;i++) { threads.emplace_back(std::thread(func1,200)); } for(auto& i:threads) { if(i.joinable()) { i.join(); } } return 0; } //因为只加了一微秒的锁,锁一解开就开始出现线程抢占资源的现象 //0 1 2 3 4 5 6 7 8 9 10 11 120 13 14 115 216 3 17 4 518 6 19 7 20 21 228 923 24 25 26 27 28 29 30 31 32 33 34 35 36 370 1 380 39 1 2 3 4 510 60 11140 7 122 2 3 48 9 5 31013 11 14 1512 441 426 743 8 44 5 //4513 14 15 4616 17 47 18 1948 20 4921 50 51 52 22 23 24916 10 1117 18 19 20 2125 22 626 7 27 8 912 10 53 54 5523 24 2528 26 272911 3013 12 1413 15 14 1656 17 57 5831 1815 19 20 2128 3259 221629 17 30 18 31 1933 20 2334 3235 33 60 36 3461 213524 36 25 37 38 26 39 27 40 284122 29 30 31 3237 33 34 35 36 3742 3823 39 2462 25 63 26 6443 38 65 44 4566 46 674039 41 4047 48 49 50 5168 52 69 5327 54 28 55 2942 30 43 44 31 32 334541 4670 4742 48 49 5034 51 35 3656 37 5738 5839 5971 60 6172 62 7343 44 45 4046 41 47 42 4348 7452 75 5376 77 54 55 63 6444 4549 5056 65 57 66 58 5946 4751 48 52 4978 50 5179 526067 61 68 6953 70 5362 54 55 56 5463 5580 56 5781 5782 58 597158 72 7364 74 75 65 7683 77 60 7866 79 6784 68 59 69 60 70 61 71 72 6261 63 646285 6380 64 73 7465 75 8681 82 876683 88 8984 7667 77 65 78 90 79 80 8166 8285 83 84 85 86 8786 88 8991 90 91 92 6892 69 937093 94 9567 876896 69 8870 89 90 91 92 93 9471 95 72 9671 9472 95 73 7497 9798 9899 9973100 101 100102 101 103 104 105102 106 103 107 75108 109 110 11176 112 113 7774 114 115104 116 117105 78106118 107 119 10896 97 9879 9912075 12180 122 123 12481 12576 12677 127 12882 129 78 13079 131100 132 83 133 84101 85 10286 87109 110 111 112 113103 10480 105 106 81107 108134 109 135 11482 115 83 13688 137 138 13989 140 141 14284 143 144110 145 146 111147116 148 85 8690 87 88 8991 11290 113117 114 115 116 1179192 92 93149 94 95 150 96 151118118 97 93119 11994 120 98120 121 99152 10015395 154 155 15696 121 122 123 124 125 157101 122 102 12310397 104 98 105 126106 127 107 108124158 125 159126 99160 100128 101 102 103 104129 130105 131 161132 162 109133 110 111106 127163 128 164 165 166129 167134 168112 113135 114 136 137115 116 117 107 169138 170 171 172118 173130 174 131 132139 133 140 134 119175 120 176 177 178121 179 122 180 123 181141 182 183108 109 110 184142 185 186 143124 144 125111 126 112 113 127145 128187 146 188129147 189 148 190114 191 192135 193130 194 131 195149 132 133196 134 135136115 137 138150 151 152 153 154 155 156 197 198139116 140 141 142117 143 118 119157 158 159 199136 137 // 120160 121 161 162 122144 138 145 139 123 146 163124 164 125 126 165 //140147166 127148 128 149 129 130167 131 150132 133151 134 152 135 153 136 137 154141 168142 169 143 170 144 155 138 156 139171 140 141145 142 143146 144 147157 172 145 158 146148 149159 150 151 160 173161 162 163174 164 165 166 167 152 153175168 169 170 147171 148154 149172 155176 156 157177 178 150179 180 151 158152 153 159154 160173 174181 175161 162176 177 178 155163 156 164 157165179 166182 167 168 169158 159183 160 161 184180 185 186170 171162 172 173163181 164 165182 183 166 174187 175 176 177167 168178188 169 179184 189 185 190 186170 180191 181171 182 187172 188 192173 183 193 184 194 185 195 186 189196174 190 197 187175198 188 199 189191 190 176 //191 192192 193 193194 177195 178 194179 195 180 196196 197 198 181199 182 // 183197 184198 185199 186 // 187 188 189 190 191 192 193 194 195 196 197 198 199
std::lock相关
可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁,c++11主要有std::lock_guard和std::unique_lock两种方式,使用方式都类似。
😆小知识
lock_guard 类是一个mutex封装者,它为了拥有一个或多个mutex而提供了一种方便的 RAII style 机制。( 译注:所谓的RAII,全称为Resource Acquisition Is Initialization,汉语是“资源获取即初始化”。但是这个直译并没有很好地解释这个词组的含义。其实含义并不高深复杂,简单说来就是,在资源获取的时候将其封装在某类的object中,利用"栈资源会在相应object的生命周期结束时自动销毁"来自动释放资源,即,将资源释放写在析构函数中。所以这个RAII其实就是和智能指针的实现是类似的。)。
当一个lock_guard对象被创建后,它就会尝试去获得给到它的mutex的所有权。当控制权不在该lock_guard对象所被创建的那个范围后,该lock_guard就会被析构,从而mutex被释放。
定义lock_guard的时候调用构造函数加锁,大括号解锁的时候调用析构函数解锁。虽然lock_guard挺好用的,但是有个很大的缺陷,在定义lock_guard的地方会调用构造函数加锁,在离开定义域的话lock_guard就会被销毁,调用析构函数解锁。这就产生了一个问题,如果这个定义域范围很大的话,那么锁的粒度就很大,很大程序上会影响效率。因此就引入了unique_lock
😆小知识
unique_lock<> unique();
这个会在构造函数加锁,然后可以利用unique.unlock()来解锁,所以当你觉得锁的粒度太多的时候,可以利用这个来解锁,而析构的时候会判断当前锁的状态来决定是否解锁,如果当前状态已经是解锁状态了,那么就不会再次解锁,而如果当前状态是加锁状态,就会自动调用unique.unlock()来解锁。而lock_guard在析构的时候一定会解锁,也没有中途解锁的功能。当然,方便肯定是有代价的,unique_lock内部会维护一个锁的状态,所以在效率上肯定会比lock_guard慢。
#include "iostream" #include "thread" #include "mutex" #include "functional" std::timed_mutex mutex; int main() { std::function < void(int) > func1 = [](int k) { std::lock_guard<std::timed_mutex> lock(mutex); for (int i = 0; i < k; i++) { std::cout << i << " "; } lock.unlock();//报错,不支持中途解锁,会导致锁的粒度太大,可以使用unique_lock std::cout << std::endl; }; return 0; }
std::atomic相关
c++11提供了原子类型std::atomic,理论上这个T可以是任意类型
#include "iostream" #include "thread" #include "mutex" #include "functional" struct NewCounter { // 使用原子变量的计数器 std::atomic<int> count; void add() { ++count; // count.store(++count);这种方式也可以 } void sub() { --count; // count.store(--count); } int get() { return count.load(); } }; int main() { return 0; }
std::call_once相关
c++11提供了std::call_once来保证某一函数在多线程环境中只调用一次,它需要配合std::once_flag使用
#include "iostream" #include "thread" #include "mutex" #include "functional" std::once_flag onceFlag; int main() { std::function<void()> func1 = []() { std::call_once(onceFlag, []() { std::cout << "call once" << std::endl; }); }; std::thread threads[5]; for (int i = 0; i < 5; ++i) { threads[i] = std::thread(func1); } for (auto &th: threads) { th.join(); } return 0; }
std::condition_variable相关
条件变量是c++11引入的一种同步机制,它可以阻塞一个线程或者个线程,直到有线程通知或者超时才会唤醒正在阻塞的线程,条件变量需要和锁配合使用,这里的锁就是上面介绍的std::unique_lock。
#include <iostream> // std::cout #include <thread> // std::thread #include <mutex> // std::mutex, std::unique_lock #include <condition_variable> // std::condition_variable std::mutex mtx; // 全局互斥锁. std::condition_variable cv; // 全局条件变量. bool ready = false; // 全局标志位. void do_print_id(int id) { std::unique_lock<std::mutex> lck(mtx); cv.wait(lck, []{ return ready; });//一定要这么写,用while还是会出现虚假唤醒,不知道为啥 // 线程被唤醒, 继续往下执行打印线程编号id. std::cout << "thread " << id << '\n'; } void go() { std::unique_lock<std::mutex> lck(mtx); ready = true; // 设置全局标志位为 true. cv.notify_one(); // 唤醒所有线程. } void vecthread() { std::thread threads[10]; // spawn 10 threads: for (int i = 0; i < 10; ++i) threads[i] = std::thread(do_print_id, i); for (auto &th: threads) th.detach(); } int main() { std::thread create_vecthread(vecthread); create_vecthread.join(); //因为join了以后必定先执行 std::cout << "10 threads ready to race...\n"; go(); // go! std::this_thread::sleep_for(std::chrono::seconds(2)); return 0; }
std::future相关
c++11关于异步操作提供了future相关的类,主要有std::future、std::promise和std::packaged_task,std::future比std::thread高级些,std::future作为异步结果的传输通道,通过get()可以很方便的获取线程函数的返回值,std::promise用来包装一个值,将数据和future绑定起来,而std::packaged_task则用来包装一个调用对象,将函数和future绑定起来,方便异步调用。而std::future是不可以复制的,如果需要复制放到容器中可以使用std::shared_future。
😆小知识
三者之间的关系
std::future用于访问异步操作的结果,而std::promise和std::packaged_task在future高一层,它们内部都有一个future,promise包装的是一个值,packaged_task包装的是一个函数,当需要获取线程中的某个值,可以使用std::promise,当需要获取线程函数返回值,可以使用std::packaged_task。
#include <iostream> #include <functional> #include <thread> #include <future> // std::promise, std::future void print_int(std::future<int>& fut) { int x = fut.get(); // 获取共享状态的值. std::cout << "value: " << x << '\n'; // 打印 value: 10. } int main () { std::promise<int> prom; // 生成一个 std::promise<int> 对象. std::future<int> fut = prom.get_future(); // 和 future 关联. std::thread t(print_int, std::ref(fut)); // 将 future 交给另外一个线程t. prom.set_value(10); // 设置共享状态的值, 此处和线程t保持同步. t.join(); return 0; }
#include <functional> #include <future> #include <iostream> #include <thread> using namespace std; int func(int in) { return in + 1; } int main() { std::packaged_task<int(int)> task(func); std::future<int> fut = task.get_future(); std::thread(std::move(task), 5).detach(); cout << "result " << fut.get() << endl; return 0; }
async相关
async是比future,packaged_task,promise更高级的东西,它是基于任务的异步操作,通过async可以直接创建异步的任务,返回的结果会保存在future中,不需要像packaged_task和promise那么麻烦,关于线程操作应该优先使用async
async具体语法如下:
async(std::launch::async | std::launch::deferred, func, args...);
功能:第二个参数接收一个可调用对象(仿函数、lambda表达式、类成员函数、普通函数…)作为参数,并且异步或是同步执行他们。
对于是异步执行还是同步执行,由第一个参数的执行策略决定:
(1)、std::launch::async 传递的可调用对象异步执行;
(2)、std::launch::deferred 传递的可调用对象同步执行;
(3)、std::launch::async | std::launch::deferred 可以异步或是同步,取决于操作系统,我们无法控制;
(4)、如果我们不指定策略,则相当于(3)。
对于执行结果:
我们可以使用get、wait、wait_for、wait_until等待执行结束,区别是get可以获得执行的结果。如果选择异步执行策略,调用get时,如果异步执行没有结束,get会阻塞当前调用线程,直到异步执行结束并获得结果,如果异步执行已经结束,不等待获取执行结果;如果选择同步执行策略,只有当调用get函数时,同步调用才真正执行,这也被称为函数调用被延迟。
#include <functional> #include <future> #include <iostream> #include <thread> #include "time.h" #include "chrono" using namespace std; int func(int in) { this_thread::sleep_for(chrono::microseconds(1000)); return in + 1; } int main() { std::future<int> res = std::async(func, 5); std::future_status status; do { status = res.wait_for(std::chrono::microseconds(0)); switch (status) { case std::future_status::ready: std::cout << "Ready..." << std::endl; //获取结果 std::cout << res.get() << std::endl; break; case std::future_status::timeout: std::cout << "timeout..." << std::endl; break; case std::future_status::deferred: std::cout << "deferred..." << std::endl; break; default: break; } } while (status != std::future_status::ready); cout << res.get() << endl; // 这里会报错,使用std::shared_future不会报错 return 0; }
std::future与std::shard_future的用途都是为了占位,但是两者有些许差别。std::future的get()成员函数是转移数据所有权;std::shared_future的get()成员函数是复制数据。 因此: future对象的get()只能调用一次;无法实现多个线程等待同一个异步线程,一旦其中一个线程获取了异步线程的返回值,其他线程就无法再次获取。 std::shared_future对象的get()可以调用多次;可以实现多个线程等待同一个异步线程,每个线程都可以获取异步线程的返回值。