C/C++(八)C++11
目录
一、C++11的简介
二、万能引用与完美转发
1、万能引用:模板中的 && 引用
2、完美转发:保持万能引用左右值属性的解决方案
三、可变参数模板
1、可变参数模板的基本使用
2、push 系列和 emplace 系列的区别
四、lambda表达式(重点)
1、函数对象(又名仿函数)
2、lambda 表达式
2.1 lambda 表达式的书写格式
1、捕捉列表
2、参数列表
3、mutable
4、->返回值类型
5、函数体
6、小总结
3、各种类型的 lambda 表达式代码示例
4、lambda表达式的一些注意事项
5、lambda表达式的底层原理
五、常用包装器:function
1、代码引入
2、function 包装器的作用
3、function 包装器的使用
3.1 模板原型
3.2 使用举例
六、通用函数适配器:bind
1、模板原型
2、使用举例(重点)
七、C++11标准线程库
0、C++标准线程库常用函数表格
1、标准线程库使用的一些注意事项
2、线程函数的参数
3、线程函数的返回值
4、原子性操作库(atomic)
4.1 C++98 线程安全解决方案:对欲修改的共享数据加锁
4.1.1 互斥锁的类型
4.1.2 加锁解锁的函数
4.1.3 死锁问题
4.1.4 lock_guard VS unique_lock(小重点)
4.2 C++11 提供的另一种线程安全解决方案:原子操作
本章将介绍继C++98以来更新最大的一个标准,也是实际开发中用的最多的重要标准,C++11标准。
由于C++11增加了非常多的语法特性,笔者学识有限,也很难一一介绍,在此主要讲解一些比较实用的语法。
一、C++11的简介
公元2003年,C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得 C++03 这个名字取代了 C++98 称为 C++11 之前的最新C++标准名称。=
不过由于 C++03(TC1) 主要是对 C++98 标准中的漏洞进行修复,并没有改动语言的核心部分,因此人们习惯性的把两个标准合并称为 C++98/03标准。
从 C++0x 到 C++11,C++标准十年磨一剑,第二个真正意义上的标准C++11才终于珊珊来迟。
相较于 C++98/03,C++11带来了数量可观的变化——其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正;这使得C++11更像是从C++98/03中孕育出的一种新语言。
相比较而言, C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,值得重点去学习。
我们在前文中已经提到了两个C++11标准中的重要内容:
《C/C++(四)类和对象》第五小节第3小点:右值引用和移动语义
《C/C++(七)RAII思想与智能指针》全文
因此关于这两篇文章所提到的有关C++11的知识点,本章在此不做赘述,诸位看官若感兴趣,可直接点击跳转至相应文章观看。
cppreference(C++学习手册)中有关C++11标准的详细介绍
二、万能引用与完美转发
1、万能引用:模板中的 && 引用
我们在 C/C++(四)中介绍了右值引用,即 &&引用,作用是给右值取别名;
但是在模板中,&& 就不是代表右值引用了,编译器会把其处理为万能引用——即既能接收左值引用,又能接收右值引用。(又名引用折叠,即传右值为右值引用,传左值 && 会被折叠成 & 即左值引用)
我们给出一段代码:
#include <iostream> using namespace std;void Fun(int& x) { cout << "左值引用" << endl; } void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) { cout << "右值引用" << endl; } void Fun(const int&& x) { cout << "const 右值引用" << endl; }template<typename T> void PerfectForward(T&& t) {Fun(t); }int main() {PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值return 0; }![]()
观察运行结果,发现全部变成了左值引用 运行结果为什么会是这样呢?
这是因为,模板的万能引用只是提供了可以同时接受左值引用和右值引用的能力,但是当把 t 直接传递给
Fun函数时,t已经是一个具有名字的变量,因此它总是被视为一个左值。如果我们想在传递参数过程中始终保持其左右值属性怎么办呢?这时候就需要完美转发了。
2、完美转发:保持万能引用左右值属性的解决方案
完美转发的关键字是 std::forward<模板类型名>( 参数名 ),作用是可以保持传参时参数的属性,传左值就是左值,传右值就是右值。
我们修改一下上面的代码:
#include <iostream> using namespace std;void Fun(int& x) { cout << "左值引用" << endl; } void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) { cout << "右值引用" << endl; } void Fun(const int&& x) { cout << "const 右值引用" << endl; }template<typename T> void PerfectForward(T&& t) {Fun(forward<T>(t)); }int main() {PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值return 0; }![]()
可以发现运行结果正确了
三、可变参数模板
1、可变参数模板的基本使用
在C++98/03中,类模板和函数模板中,只能包含固定数量的模板参数;
而C++11引入了新特性可变参数模板,让我们可以接受可变参数的函数模板与类模板。
由于可变参数模板使用比较抽象晦涩,在此点到为止,只介绍一些基础的可变参数模板特性便足够使用了。
下面这个就是一个基本可变参数的函数模板:
// Args是一个模板可变参数包,表示模板可以接受任意数量和类型的参数 template <class ...Args>// args是一个函数可变参数包,表示函数可以接受任意数量和类型的参数。 void Test(Args... args) {}可以看到,模板可变参数是在模板类型名前面加上 ... ,函数可变参数就是在类型名后面加上 ... 。
我们把带省略号的参数称作“参数包”,里面可以包含任意个参数。
2、push 系列和 emplace 系列的区别
C++使用手册中vector容器的emplace_back函数
C++使用手册中 list 容器的emplace_back函数
template <class... Args> void emplace_back (Args&&... args);我们可以看到,emplace 系列的接口,相较于push 系列,都是支持万能引用与可变参数模板的。
#include <iostream> #include <list> using namespace std;int main() {// 下面我们试一下带有拷贝构造和移动构造的 string 类型// 我们会发现差别:emplace_back是直接构造了,push_back是先构造,再移动构造std::list< std::pair<int, string> > mylist;// 多参数时,可以分开一个个传参(因为 emplace_back 的形参是可变参数包,直接把参数包不断往下传,直接构造到节点上)mylist.emplace_back(10, "sort");mylist.emplace_back(make_pair(20, "sort"));// 隐式类型转换,先接受参数构造,再移动构造mylist.push_back(make_pair(30, "sort"));mylist.push_back({ 40, "sort" });for (auto e : mylist){cout << e.first << ":" << e.second << endl;}return 0; }所以:
push系列接口:适用于已经存在的对象,需要先构造对象再复制或移动到容器中。emplace系列接口:适用于直接在容器中构造对象,避免了不必要的临时对象创建和销毁,提高了性能。(浅拷贝时以及参数较多时相较于 push 系列性能提升巨大,深拷贝时也会高效一些,但提升幅度没有这么大)
四、lambda表达式(重点)
1、函数对象(又名仿函数)
函数对象,又名仿函数,即可以像函数一样使用的对象,实际上就是在类里面重载了 operator() 运算符的类对象。
在C++98中,如果想要为某些自定义元素排序,就需要利用仿函数来定义排序时的比较规则
#include <iostream> #include <algorithm> #include <vector> using namespace std;struct Goods {Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}string _name; // 名字double _price; // 价格int _evaluate; // 评价 }; struct ComparePriceLess {// 价格倒序排序bool operator()(const Goods& gl, const Goods& gr){return gl._price < gr._price;} }; struct ComparePriceGreater {bool operator()(const Goods& gl, const Goods& gr){// 价格正序排序return gl._price > gr._price;} };int main() {vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());}我们可以发现,为了实现自定义类型元素的排序,针对每一种排序规则都要去实现一个类,类里还要重载operator(),显得有些不便。
因此,C++11中出现了lambda表达式用来轻量级代替函数对象
2、lambda 表达式
2.1 lambda 表达式的书写格式
[ 捕捉列表 ](参数列表)mutable ->返回值类型{函数体}1、捕捉列表
捕捉列表总是出现在 lambda 表达式的开头,编译器根据捕捉列表来判断接下来的代码是否为 lambda 函数。(因此必须要写,不能省略)
作用:可以根据列表捕捉项来捕捉 lambda 表达式所在的函数的作用域中的可用变量,使其直接成为 lambda 函数的成员变量。
列表捕捉项:描述了上下文中哪些数据可以被lambda函数所使用,以及传参的方式是传值还是传引用。
有哪些列表捕捉项呢:
[变量名]:表示传值捕捉某变量,并传值传参,传值捕捉来的变量不可被修改。
[&变量名]:以引用的方式捕捉某变量,并传引用传参。(注意不要与取地址混淆了,只有在捕捉列表里是引用传递捕捉变量)
[=]:表示传值捕捉并传参包含 lambda表达式的代码块中的所有变量。(包括 this)
[&]:表示传引用捕捉并传参包含 lambda表达式的代码块中的所有变量。(包括 this)
[this]:表示值传递方式捕捉当前 this 指针。
2、参数列表
lambda 表达式的参数列表与普通函数的参数列表一致,如果不需要传参,可以连同括号一起省略。
3、mutable
在默认情况下, lambda函数中按值捕获的变量不可被修改。
添加 mutable 关键字,可以取消 lambda 函数中按值捕获的变量的常量性,使之可以被修改不想取消可以省略(注意:如果使用了该修饰符,参数列表将不能省略)
4、->返回值类型
采用追踪返回的形式声明函数返回值类型。如果没有返回值 / 返回值类型可以由编译器明确推导出来,可以省略。
5、函数体
与普通函数的函数体一致,必须要写。(该函数体内除了可以使用参数列表中的参数,还可以使用由捕捉列表捕捉到的变量)
6、小总结
在 lambda 表达式的定义里,(参数列表)、mutable、->返回值类型都是可以省略的部分,而捕捉列表与函数体不能省略。(因此,最简单的 lambda 表达式就是:[]{},但是没有任何意义)
3、各种类型的 lambda 表达式代码示例
#include <iostream> using namespace std;int main() {// 1、最简单的lambda表达式[] {};// 2、省略参数列表,mutable和返回值类型,返回值类型由编译器推导出来int a = 3, b = 4;[=] {return a + b; };// 3、省略返回值类型(无返回值类型)和mutableauto fun1 = [&](int c) {return a + c; }; // 注意,lambda表达式必须要有变量来接收,才能使用fun1(10); // 使用也是类似于函数cout << a << " " << b << endl;// 4、各方面都比较完善的lambda表达式auto fun2 = [=, &b](int c)->int {return b += (a + c); };fun2(1);cout << b << endl;// 5、传值捕捉x,添加mutable使之能被修改int x = 10;auto fun3 = [x](int a)mutable->int { a += 2; x *= 2; return x + a; }; }值得注意的是,lambda表达式实际上可以被理解为无名函数,不能直接调用,想要直接调用,必须通过 auto 赋值给一个变量。(auto 也是C++11更新的关键字,用于自动推导类型)
4、lambda表达式的一些注意事项
1、捕捉列表可由多个捕捉项组成,并以逗号分割。
(eg:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量)
2、捕捉列表不允许变量重复传递,否则就会导致编译错误。
(eg:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复)
3、在代码块作用域以外的lambda函数捕捉列表必须为空。
4、在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者
非局部变量都会导致编译报错。
5、lambda表达式之间不能相互赋值,即使看起来类型相同
5、lambda表达式的底层原理
#include <iostream> using namespace std;class Rate { public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;} private:double _rate; };int main() {// 函数对象double rate = 0.49;Rate r1(rate);r1(10000, 2);// lambda表达式auto r2 = [=](double monty, int year)->double {return monty * rate * year; };r2(10000, 2);return 0; }之前说过,从使用方式来看,lambda表达式的使用方式与函数对象完全一样。
在这个代码里,函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表则可以直接将该变量捕获到。
其实,底层编译器对 lambda 表达式的处理方式就是完全比照函数对象来的,底层每一个lambda 表达式都会生成一个仿函数类。
![]()
可以发现,C++底层,lambda表达式与仿函数的处理完全一致
五、常用包装器:function
fuction 包装器又称适配器;C++中的function本质是一个类模板,是为了解决模板低效问题而出现的,可以统一类型
1、代码引入
#include <iostream> using namespace std;template <class F, class T>T useF(F f, T x) {static int count = 0;cout << "count: " << count++ << endl;cout << "count: " << &count << endl;return f(x); }double f(double i) {return i / 2; }struct Functor {double operator()(double d){return d / 3;} };int main() {// useF的模板参数做函数名cout << useF(f, 10.27) << endl;// useF的模板参数做仿函数cout << useF(Functor(), 10.27) << endl;// useF的模板参数做lambda表达式cout << useF([](double d)->double {return d / 4; }, 10.27) << endl;return 0; }
观察这段程序的运行结果我们可以发现,一套模板可以调用的类型非常丰富:函数指针、仿函数、lambda表达式。
但是他们的运行却是相互独立的,每一种类型都会实例化出一份模板,这会造成模板效率下降。
应该如何解决呢?使用 function 包装器统一类型。
2、function 包装器的作用
函数指针、仿函数、lambda表达式这三大可调用对象各有一定的缺陷:
函数指针用起来比较复杂苦涩而且局限性很大;仿函数比较臃肿笨重而且要全局定义;lambda 表达式又无法判断其类型只能依靠 auto 让编译器自己推导。
因此 function 包装器就出现了,其作用就是把可调用对象给包装起来,对类型进行统一,同时解决模板低效问题。(PS:类成员函数也属于可调用对象,也可以用function 将其包装起来)(但是 function 本身并不是可调用对象,需要包装可调用对象才能使用)
3、function 包装器的使用
前言:使用std::function 需要包含头文件 <functional>
3.1 模板原型
template <class T> function;template <class Ret, class ...Args> class function<Ret(Args...)>;/*模板参数说明:Ret:被调用对象的返回类型Args:被调用对象的形参,是个可变参数列表,可以包装各式参数 */3.2 使用举例
#include <iostream> #include <functional> using namespace std;template <class F, class T>// 函数模板 T useF(F f, T x) {static int count = 0;cout << "count: " << ++count << endl;cout << "count: " << &count << endl;return f(x); }// 函数 double f(double i) {return i / 2; }// 仿函数 struct Functor {double operator()(double d){return d / 3;} };class Test { public:double minus(double x, double y){return x - y;}double add(double x, double y){return x + y;} }; int main() {// 1、function包装函数名(函数指针)function<double(double)> func1 = f;cout << useF(func1,10.27) << endl;// 2、function包装仿函数function<double(double)> func2 = Functor();cout << useF(func2, 10.27) << endl;// 3、function包装lambda表达式function<double(double)> func3 = [](double x)->double {return x; };cout << useF(func3, 10.27) << endl;// 4、function包装类成员函数// 注意:类成员函数的包装比较特殊,可变参数包需要多一个this指针,底层通过这个this指针调用函数;包装的可调用函数也需要加类域 和 &function<double(Test, double, double)> func4 = &Test::add;cout << func4(Test(), 20.15, 10.27) << endl;return 0; }
观察运行结果可以发现,函数模板只实例化了一份,实现了类型统一。
六、通用函数适配器:bind
std::bind 同样定义在 <functional> 头文件中,就像一个函数适配器,接收可调用对象(一般与 function 搭配),生成新的可调用对象来“适应”源对象参数列表,多用来调节可调用对象参数的个数和顺序,让可调用对象的参数使用更加灵活。
一般而言,我们可以用 bind 把一个原本接收n个参数的函数,通过绑定一些参数,返回一个可以只接收其中部分参数的新函数。
1、模板原型
// 模板1:不指定返回类型,通用性较强,编译器通过fn的类型自动推断
template <class Fn, class ...Args>
bind(Fn&& fn, Args&&... args);// 模板2:指定返回类型Ret,使用的时候需要bind<Ret>显式指明返回类型,防止返回类型出错
template <class Ret, class Fn, class ...Args>
bind(Fn&& fn, Args&&... args);/*参数类型说明:fn:万能引用,传可调用对象args:可变参数包
*/
其中模板1是最常用的,因此我们调用 bind 的一般形式是:
auto NewCallable = bind(callable, arg_list);
其中,NewCallable 本身也需要是一个可调用对象;arg_list 是一个逗号分隔的参数列表,对应callable 中的参数。(注意:arg_liist 里面大概率会出现 _n (n为正整数)的占位符,用来表示参数在 NewCallable 中的位置,_1为newCallable的第一个参数,_2为第二个参数,以此类推)
当我们调用NewCallable时,NewCallable 会调用 callable,并把arg_list 中的参数传给它。
2、使用举例(重点)
#include <iostream>
#include <functional>
using namespace std;int add(int x, int y)
{return x + y;
}class Test
{
public:int minus(int x, int y){return x - y;}
};int main()
{// func1 绑定函数 add,且参数由调用 func1, func2 的第一、二个参数指定function<int(int, int)> func1 = bind(add, placeholders::_1, placeholders:: _2);cout << func1(2014, 2015) << endl;// func2 也绑定函数add,但是参数明确给出来(func2 的类型由于两个参数全部写死,类型实际上变成了function<int()>)function<int()> func2 = bind(add, 2014, 2015);cout << func2() << endl;// func3 绑定成员函数(绑定成员函数,需要有对应实例来调用,同时需要取地址,确认是哪个类的哪个成员函数,不取编译器无法识别)Test t;function<int(int, int)> func3 = bind(&Test::minus, t, placeholders::_1, placeholders::_2);cout << func3(2015, 2014) << endl;// func4 改变参数的位置(即func4的第一个参数由传递的第二个参数决定,反之亦然)function<int(int, int)> func4 = bind(&Test::minus, t, placeholders::_2, placeholders::_1);cout << func4(2015, 2014) << endl;// func5 也可以调整传参的个数// 可以把每次都要固定传的参数给绑死,在调用的时候简化调用。由于写死了一个形参,所以function的类型也跟着改变为function<int(int)>function<int(int)> func5 = bind(&Test::minus, t, 2013, placeholders::_1);cout << func5(2012) << endl;return 0;
}
七、C++11标准线程库
在C++11之前,涉及到多线程问题,都是和平台相关的,比如在Windows和Linux下各有自己的多线程接口,这使得代码的可移植性较差。
因此C++11中最重要的特性就是提供了对线程的支持,使得C++在并行编程时不需要再依赖第三方库了。(同时在原子操作中还引入了原子类的概念)
使用C++11标准线程库,需要包含头文件 <thread>
C++使用手册中关于 thread 线程类的详细介绍
| thread() | 无参构造函数,构造一个没有关联任何线程函数的线程对象,不会启动任何线程 |
| thread(fn,args1,args2……) | 构造出一个线程对象,该对象关联了可执行对象 fn,args1、args2、……为线程函数的参数 |
| get_id() | 获取 thread 类型的线程ID |
| joinable() | 判断线程是否还在执行,如果线程对象关联了一个正在执行的线程,返回 true; 如果线程对象没有关联任何线程,或者线程已经结束已经被回收,返回 false |
| join() | 会使当前线程阻塞,直到被调用的线程完全执行完毕,子线程结束后还会自动回收子线程的资源;用来保证线程同步 |
| detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离 的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
1、标准线程库使用的一些注意事项
1、线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程和获取线程状态。
(也因为线程对象的存在,可以把线程以对象的形式往容器里面放)
2、创建一个线程对象后,如果没有提供线程函数,该对象实际没有对应任何线程。
3、如果创建一个线程对象,并且给该线程对象关联线程函数,程序运行后,该线程就会被启动,与主线程一起并发运行。(线程函数一般关联可调用对象:函数指针、lambda表达式、函数对象)
4、thread线程类是防拷贝的,不允许拷贝构造以及赋值,但是支持移动构造和移动赋值。(即将一个线程对象关联的线程转移给其他线程对象,转移期间不影响线程的执行)
5、可以通过 jionable()函数来判断线程是否有效,如果是以下任意情况,则线程无效:
采用无参构造函数构造的线程对象
线程对象的状态已经转移给其他线程对象
线程已经调用jion或者detach结束
2、线程函数的参数
线程函数在默认传参的时候,是以传值方式传参的,想要保留其引用特性,需要使用 std::ref(参数名)函数。(当然传指针没有这种问题)
(只要线程函数参数是左值引用,传参的时候,必须使用 ref() 函数,把参数包装成引用,才能保证左值引用正确传参——底层是先把参数传给 thread 线程类的构造函数,再传给线程函数,由于线程库底层某些复杂的原因,所有的参数传过去都会变成右值。)
#include <iostream>
#include <thread>
using namespace std;void ThreadFunc1(int& x)
{x += 10;
}
void ThreadFunc2(int* x)
{(*x) += 10;
}void ThreadFunc3(const string& s)
{cout << s << endl;
}int main()
{int x = 2015;// 通过 ref 函数保持其引用属性,否则会因为最后结果是右值导致报错没有对应函数thread t1(ThreadFunc1, ref(x));t1.join();cout << x << endl;// 传指针,不存在此类问题thread t2(ThreadFunc2, &x);t2.join();cout << x << endl;// 传右值就可以,不会报错string s = "二零一五";thread t3(ThreadFunc3, s);t3.join();return 0;
}
3、线程函数的返回值
我们一般很难拿到线程函数的返回值,如果想得到某种结果,可以在线程函数中多加一个输出型参数用来承载结果。
4、原子性操作库(atomic)
多线程最主要的问题是共享数据带来的问题(即线程安全问题)。如果共享数据都是只读的,那么没问 题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数 据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的问题。
那么应该如何解决线程安全问题呢?
4.1 C++98 线程安全解决方案:对欲修改的共享数据加锁
4.1.1 互斥锁的类型
C++ 中有许多互斥锁类,他们有各自的作用
C++中互斥锁类的类型 mutex 最常用的普通互斥锁 recursive_mutex 递归互斥锁,在递归的时候使用,因为一般的互斥锁在递归时会死锁(底层简单来说就是在锁资源被占用后,不会第一时间阻塞,而是先判断是不是当前线程,如果不是,就不阻塞,可以继续执行) timed_mutex 时间互斥锁,可以设置超时时间(相对/绝对,一般用相对),到了之后锁才可以加/解锁 recursive_timed_mutex 递归时间互斥锁 4.1.2 加锁解锁的函数
C++加锁解锁的函数也有许多
lock() 最基础且常用的阻塞式加锁,如果锁资源被占用,线程在此阻塞 try_lock() 防阻塞式锁,先试一下能不能加锁,判断一下锁资源有没有被占用,如果没有就上锁,不能字节返回,不会向 lock()一样阻塞线程
try_lock_for(std::chrono::duration<Rep, Period> rel_time)timed_mutex 时间互斥锁专属阻塞式锁;在设置的相对超时时间(只要写一下秒数即可,因此时间互斥锁加锁常用这个而不是绝对超超时时间)里尝试获取锁,如果锁已被占用,则阻塞当前线程,直到超时时间到达或锁可用 try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time):timed_mutex 时间互斥锁专属阻塞式锁;在设置的绝对超时时间(即具体时间)里尝试获取锁,如果锁已被占用,则阻塞当前线程,直到超时时间到达或锁可用 unlock() 解锁 4.1.3 死锁问题
C++中存在抛异常机制,一旦发生抛异常,会跳转到捕获异常处,后面的代码不再执行,这可能会导致锁资源没有被及时释放,造成死锁:
#include <iostream> #include <thread> #include <mutex> #include <cstdlib> // 包含 srand 和 rand #include <ctime> // 包含 timeusing namespace std;// 实现一个抛异常函数 void ThrowException() {if (rand() % 6 == 0){throw "异常";} }int main() {mutex mtx;int num = 100; // 循环取随机数的次数// 初始化随机数生成器srand(static_cast<unsigned int>(time(nullptr)));thread t1([num, &mtx]() {try{for (int i = 0; i < num; i++){mtx.lock();ThrowException();mtx.unlock();}}catch (const char* s){printf("这是线程1的异常: %s\n", s);}});thread t2([num, &mtx]() {try{for (int i = 0; i < num; i++){mtx.lock();ThrowException();mtx.unlock();}}catch (const char* s){printf("这是线程2的异常: %s\n", s);}});// 等待线程结束t1.join();t2.join();return 0; }![]()
可以看到,线程1先抢占了资源,然后由于抛异常导致锁资源没有释放,导致线程2一直阻塞,造成死锁 那么应该如何解决呢?这就要用到我们 C/C++(七)中所提到的重要思想:RAII思想了,把资源交给一个类对象管理,借助其析构函数进行解锁释放空间等操作。
#include <iostream> #include <thread> #include <mutex> #include <cstdlib> // 包含 srand 和 rand #include <ctime> // 包含 timeusing namespace std;// 实现一个抛异常函数 void ThrowException() {if (rand() % 6 == 0){throw "异常";} }// 利用RAII思想实现一个类用于托管资源 template <class Lock> class LockGuard { public:LockGuard(Lock& mtx):_mtx(mtx){_mtx.lock();}~LockGuard(){_mtx.unlock();} private:Lock& _mtx; //注意是引用类型,并不创建新锁 }; int main() {mutex mtx;int num = 100; // 循环取随机数的次数// 初始化随机数生成器srand(static_cast<unsigned int>(time(nullptr)));thread t1([num, &mtx]() {try{for (int i = 0; i < num; i++){LockGuard<mutex> lg(mtx);ThrowException();}}catch (const char* s){printf("这是线程1的异常: %s\n", s);}});thread t2([num, &mtx]() {try{for (int i = 0; i < num; i++){LockGuard<mutex> lg(mtx);ThrowException();}}catch (const char* s){printf("这是线程2的异常: %s\n", s);}});// 等待线程结束t1.join();t2.join();return 0; }![]()
这时候就可以正常运行了 PS:这里的RAII资源托管类不需要手搓,C++11提供了两个模板类:lock_guard 和 unique_lock
4.1.4 lock_guard VS unique_lock(小重点)
lock_guard模板类:只支持构造函数和析构函数,拷贝构造被禁止,且使用方式很单一,只能在构造函数和析构函数的时候进行加锁和解锁。
unique_lock模板类:同样不支持拷贝构造函数,但是支持手动加锁和解锁,使用方式更加灵活,提供了更多的成员函数。
某位前辈大佬整理的完整版 lock_guard 与 unique_lock 的区别
4.2 C++11 提供的另一种线程安全解决方案:原子操作
加锁虽然可以解决线程安全问题,但是很多时候用的都是阻塞锁,会影响程序的运行效率;而且还可能会出现死锁问题。
所以 C++11 就提供了原子操作(即不可被中断的一个/一系列操作)和原子类型,(必须包含头文件 <atomic>)
(PS:不过原子操作一般都是用在临界区较小,操作较短的内置类型操作当中,不需要加锁。线程可以直接对原子类型变量进行互斥地访问;对于较长的操作 / 自定义类型的操作就不建议使用原子操作了,因为原子操作通常会使用自旋锁(spin lock),这意味着线程在请求资源失败后,会不断尝试获取资源,而不是阻塞等待。如果操作时间较长,这种自旋锁会导致 CPU 资源浪费)
![]()
内置类型对应原子类型表,大部分原子类型就是前面加个atomic_ #include <iostream> #include <thread> #include <atomic> using namespace std;void fun(atomic<long>& num, size_t count) {for (size_t i = 0; i < count; i++){num++; // 原子操作,不需要加锁也能实现互斥访问} }int main() {atomic<long> num = 0;cout << "原子操作前,num = " << num << std::endl;thread t1(fun, ref(num), 1000000);thread t2(fun, ref(num), 1000000);t1.join();t2.join();cout << "原子操作后, num = " << num << std::endl;return 0; }
相关文章:
C/C++(八)C++11
目录 一、C11的简介 二、万能引用与完美转发 1、万能引用:模板中的 && 引用 2、完美转发:保持万能引用左右值属性的解决方案 三、可变参数模板 1、可变参数模板的基本使用 2、push 系列和 emplace 系列的区别 四、lambda表达式…...
使用three.js 实现 自定义绘制平面的效果
使用three.js 实现 自定义绘制平面的效果 预览 import * as THREE from three import { OrbitControls } from three/examples/jsm/controls/OrbitControls.jsconst box document.getElementById(box)const scene new THREE.Scene()const camera new THREE.PerspectiveCam…...
玩转Docker | 使用Docker部署捕鱼网页小游戏
玩转Docker | 使用Docker部署捕鱼网页小游戏 一、项目介绍项目简介项目预览二、系统要求环境要求环境检查Docker版本检查检查操作系统版本三、部署捕鱼网页小游戏下载镜像创建容器检查容器状态下载项目内容查看服务监听端口安全设置四、访问捕鱼网页小游戏五、总结一、项目介绍…...
第2章 Android App开发基础
第 2 章 Android App开发基础 bilibili学习地址 github代码地址 本章介绍基于Android系统的App开发常识,包括以下几个方面:App开发与其他软件开发有什么不一 样,App工程是怎样的组织结构又是怎样配置的,App开发的前后端分离设计…...
通过 SYSENTER/SYSEXIT指令来学习系统调用
SYSENTER指令—快速系统调用 指令格式没有什么重要的内容,只有opcode ,没有后面的其他字段 指令的作用: 执行快速调用到特权级别0的系统过程或例程。SYSENTER是SYSEXIT的配套指令。该指令经过优化,能够为从运行在特权级别3的用户代码到特权级别0的操作系统或执行过程…...
Nginx开发实战——网络通信(一)
文章目录 Nginx开发框架信号处理函数的进一步完善(避免僵尸子进程)(续)ngx_signal.cxxngx_process_cycle.cxx 网络通信实战客户端和服务端1. 解析一个浏览器访问网页的过程2.客户端服务器角色规律总结 网络模型OSI 7层网络模型TCP/IP 4层模型3.TCP/IP的解释和比喻 最…...
w外链如何跳转微信小程序
要创建外链跳转微信小程序,主要有以下几种方法: 使用第三方工具生成跳转链接: 注册并登录第三方外链平台:例如 “W外链” 等工具。前往该平台的官方网站,使用手机号、邮箱等方式进行注册并登录账号。选择创建小程序外…...
获取平台Redis各项性能指标
业务场景 在XXXX项目中把A网的过车数据传到B网中,其中做了一个业务处理,就是如果因为网络或者其他原因导致把数据传到B网失败,就会把数据暂时先存到redis里,并且执行定时任务重新发送失败的。 问题 不过现场的情况比较不稳定。出…...
STM32 HAL 点灯
首先从点灯开始 完整函数如下: #include "led.h" #include "sys.h"//包含了stm32f1xx.h(包含各种寄存器定义、中断向量定义、常量定义等)//初始化GPIO口 void led_init(void) {GPIO_InitTypeDef gpio_initstruct;//打开…...
【http作业】
1.关闭防火墙 [rootlocalhost ~]# systemctl stop firewalld #关闭防火墙 [rootlocalhost ~]# setenforce 0 2.下载nginx包 [rootlocalhost ~]# mount /dev/sr0 /mnt #挂载目录 [rootlocalhost ~]# yum install nginx -y #下载nginx包 3.增加多条端口 [rootlocalhost ~]# n…...
WPF+MVVM案例实战(十一)- 环形进度条实现
文章目录 1、运行效果2、功能实现1、文件创建与代码实现2、角度转换器实现3、命名空间引用3、源代码下载1、运行效果 2、功能实现 1、文件创建与代码实现 打开 Wpf_Examples 项目,在Views 文件夹下创建 CircularProgressBar.xaml 窗体文件。 CircularProgressBar.xaml 代码实…...
简述MCU微控制器
目录 一、MCU 的主要特点: 二、常见 MCU 系列: 三、应用场景: MCU 是微控制器(Microcontroller Unit)的缩写,指的是一种小型计算机,专门用于嵌入式系统。它通常集成了中央处理器(…...
微服务的雪崩问题
微服务的雪崩问题: 微服务调用链路中的某个服务故障,引起整个链路种的所有微服务都不可用。这就是微服务的雪崩问题。(级联失败),具体表现出来就是微服务之间相互调用,服务的提供者出现阻塞或者故障&#x…...
Java基础(4)——构建字符串(干货)
今天聊Java构建字符串以及其内存原理 我们先来看一个小例子。一个是String,一个是StringBuilder. 通过结果对比,StringBuilder要远远快于String. String/StringBuilder/StringBuffer这三个构建字符串有什么区别? 拼接速度上,StringBuilder…...
logback日志脱敏后异步写入文件
大家项目中肯定都会用到日志打印,目的是为了以后线上排查问题方便,但是有些企业对输出的日志包含的敏感(比如:用户身份证号,银行卡号,手机号等)信息要进行脱敏处理。 哎!我们最近就遇到了日志脱敏的改造。可…...
电容的基本知识
1.电容的相关公式 2.电容并联和串联的好处 电容并联的好处: 增加总电容值: 并联连接的电容器可以增加总的电容值,这对于需要较大电容值来滤除高频噪声或储存更多电荷的应用非常有用。 改善频率响应: 并联不同的电容值可以设计一个滤波器,以在特定的频率范围内提供更好的滤…...
【Axure高保真原型】分级树筛选中继器表格
今天和大家分享分级树筛选中继器表格的原型模板,点击树的箭头可以展开或者收起子级内容,点击内容,可以筛选出该内容及子级内容下所有的表格数据。左侧的树和右侧的表格都是用中继器制作的,所以使用也很方便,只需要在中…...
STM32 I2C通信:硬件I2C与软件模拟I2C的区别
文章目录 STM32 I2C通信:硬件I2C与软件模拟I2C的区别。一、硬件I2C速度快:实现简单:稳定性好: 二、软件模拟I2C灵活性高:支持多路通信: 三、选择哪种方式? STM32 I2C通信:硬件I2C与软…...
服务器新建用户
文章目录 前言一、步骤二、问题三、赋予管理员权限总结 前言 环境: 一、步骤 创建用户需要管理员权限sudo sudo useradd tang为用户设置密码 sudo passwd tang设置密码后,可以尝试使用 su 切换到 tang 用户,确保该用户可以正常使用&#…...
鸿蒙开发融云demo发送图片消息
鸿蒙开发融云demo发送图片消息 融云鸿蒙版是不带UI的,得自己一步步搭建。 这次讲如何发送图片消息,选择图片,显示图片消息。 还是有点难度的,好好看,好好学。 一、思路: 选择图片用:photoVie…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...
C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
Vue 模板语句的数据来源
🧩 Vue 模板语句的数据来源:全方位解析 Vue 模板(<template> 部分)中的表达式、指令绑定(如 v-bind, v-on)和插值({{ }})都在一个特定的作用域内求值。这个作用域由当前 组件…...
C++实现分布式网络通信框架RPC(2)——rpc发布端
有了上篇文章的项目的基本知识的了解,现在我们就开始构建项目。 目录 一、构建工程目录 二、本地服务发布成RPC服务 2.1理解RPC发布 2.2实现 三、Mprpc框架的基础类设计 3.1框架的初始化类 MprpcApplication 代码实现 3.2读取配置文件类 MprpcConfig 代码实现…...
Linux 下 DMA 内存映射浅析
序 系统 I/O 设备驱动程序通常调用其特定子系统的接口为 DMA 分配内存,但最终会调到 DMA 子系统的dma_alloc_coherent()/dma_alloc_attrs() 等接口。 关于 dma_alloc_coherent 接口详细的代码讲解、调用流程,可以参考这篇文章,我觉得写的非常…...
智能职业发展系统:AI驱动的职业规划平台技术解析
智能职业发展系统:AI驱动的职业规划平台技术解析 引言:数字时代的职业革命 在当今瞬息万变的就业市场中,传统的职业规划方法已无法满足个人和企业的需求。据统计,全球每年有超过2亿人面临职业转型困境,而企业也因此遭…...


