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、省略返回值类型(无返回值类型)和mutable
	auto 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 线程类的详细介绍

0、C++标准线程库常用函数表格

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>    // 包含 time

using 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>    // 包含 time

using 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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/904283.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

海亮科技亮相第84届中国教装展 尽显生于校园 长于校园教育基因

10月25日&#xff0c;第84届中国教育装备展示会&#xff08;以下简称“教装展”&#xff09;在昆明滇池国际会展中心开幕。作为国内教育装备领域规模最大、影响最广的专业展会&#xff0c;本届教装展以“数字赋能教育&#xff0c;创新引领未来”为主题&#xff0c;为教育领域新…

MYSQL期中复习

MYSQL [语句不要拼错&#xff0c;表名、列名不要写错&#xff0c;语句难记要记住] 创建表 模版 create table 表名(列名1 数据类型 [约束], 列明2 数据类型 [约束], [表级约束]); 约束 单一主码约束 primary key 联合主码约束 primary key(列名1,列名2) [要在列名12定义后…

结合Intel RealSense深度相机和OpenCV来实现语义SLAM系统

结合Intel RealSense深度相机和OpenCV来实现语义SLAM系统是一个非常强大的组合。以下是一个详细的步骤指南&#xff0c;帮助你构建这样一个系统。 硬件准备 Intel RealSense深度相机&#xff1a;例如D415、D435或L515。计算平台&#xff1a;一台具有足够计算能力的计算机&…

无人机之多源信息融合算法篇

一、概述 多源信息融合算法在无人机导航领域中扮演着越来越重要的角色。该算法通过整合来自不同传感器&#xff08;如全球定位系统GPS、惯性导航系统INS、磁力计、气压高度计、视觉传感器等&#xff09;的数据&#xff0c;利用先进的数据融合算法处理这些多源信息&#xff0c;以…

【Spring Boot】元注解

元注解 1.元注解1.1 Target1.2 Retention1.3 Inherited1.4 Documented1.5 interface 2.自定义注解2.1 创建自定义注解类2.2 实现业务逻辑2.3 使用自定义注解 1.元注解 元注解就是定义注解的注解&#xff0c;是 Java 提供的用于定义注解的基本注解。 注解 说明 Retention是注解…

索尔德 APON无线工业轨道机车定位测距仪介绍

索尔德APON无线定位测距仪&#xff0c;简称APON&#xff0c;采用先进的应答式微波测距技术&#xff0c;为车辆赋予了一双敏锐的“智慧之眼”&#xff0c;能够精确捕捉到有轨移动车辆的绝对位置&#xff0c;无论是快速穿梭还是缓慢移动&#xff0c;确保它们能够准确无误地抵达预…

企业如何选择适合自己的智能扭矩系统Torque?_SunTorque

【大家好&#xff0c;我是唐Sun&#xff0c;唐Sun的唐&#xff0c;唐Sun的Sun。一站式数智工厂解决方案服务商】 一、选择适合自己企业的智能扭矩系统时&#xff0c;可以考虑以下几个关键因素&#xff1a; 扭矩精度要求 首先要明确企业生产过程中对扭矩精度的具体要求。如果产…

全面解析:轻松掌握多模态技术精髓

多模态检索 多模态检索是指利用多种数据模态&#xff08;如文本、图像、视频、音频等&#xff09;进行信息检索的技术。它旨在通过整合不同形式的数据&#xff0c;提供更全面、精确和丰富的检索结果&#xff0c;以满足用户多样化的查询需求。 接下来分三部分&#xff1a; 单模…

net 获取本地ip地址,net mvc + net core 两种

net mvc public static string GetIP(HttpRequestBase request){// 尝试获取 X-Forwarded-For 头string result request.Headers["X-Forwarded-For"]?.Split(,).FirstOrDefault()?.Trim();if (string.IsNullOrEmpty(result)){// 获取用户的 IP 地址result reques…

云存储的费用是多少?2024年最新价格表

云存储的费用是多少最新&#xff1f;云存储的费用通常基于多个因素确定&#xff0c;包括存储容量、访问流量、请求次数、服务类型&#xff08;如对象存储、文件存储、块存储等&#xff09;、计费方式&#xff08;按量计费或包年包月&#xff09;以及可能的附加功能&#xff08;…

linux 原子操作

首先是为什么要有 原子操作 网上的截图&#xff1a; 不能从C语言来看&#xff0c;要从汇编来看 但是实际的情况有可能是这样。 A进程没有得到想要的结果。 然后是 原子操作的 底层实现 最终会是这段代码&#xff0c;当然只是一个 加一的操作。 static inline void atomic_a…

从0到1构建 UniApp + Vue3 + TypeScript 移动端跨平台开源脚手架

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f343; vue-uniapp-template &#x1f33a; 仓库主页&#xff1a; GitCode&#x1f4ab; Gitee &#x1f…

解析日期、编码

解析日期 这里指的是将字符串或者object类型的日期&#xff0c;转换成panda或python的日期类型。 主要的是dtype的变化&#xff1a;object / str —> datetime64[ns] # modules well use import pandas as pd import numpy as np import seaborn as sns import datetime# …

swiper默认显示三个,中间放大且显示全部图片两边显示部分图片

先上效果图 template <template><div><div class"swiper-content"><div class"swiper-container"><div class"swiper-wrapper"><div class"swiper-slide"><img src"../../assets/images/…

【每日刷题】Day146

【每日刷题】Day146 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. mari和shiny 2. 组队竞赛__牛客网 3. 删除相邻数字的最大分数_牛客题霸_牛客网 1. mari和shiny /…

Resnet搭建介绍及代码撰写详解(总结6)

可以从本人以前的文章中可以看出作者以前从事的是嵌入式控制方面相关的工作&#xff0c;是一个机器视觉小白&#xff0c;之所以开始入门机器视觉的学习主要是一个idea&#xff0c;想把机器视觉与控制相融合未来做一点小东西。废话不多说开始正题。&#xff08;如有侵权立即删稿…

compose.material3 中的DatePicker在 desktop 平台同样可以适用

引入 implementation(compose.material3) 添加触发 OptIn(ExperimentalMaterial3Api::class)Composableprivate fun BasicDateUnit(label: String, selectedDateStr: MutableState<String>) {var showDatePicker by remember { mutableStateOf(false) }var selectedDate…

115页PPT集团管控模型与企业实践5D

01 115页PPT集团管控模型与企业实践5D “5D1C”模型是一种集团管控框架&#xff0c;它将集团管控的主要任务划分为五个方面以及一个核心&#xff0c;即战略&#xff08;Strategy&#xff09;、组织&#xff08;Organization&#xff09;、决策&#xff08;Decision&#xff09…

创客匠人老蒋:创始人自己做服务,才有市场敏感度

大家好&#xff0c;我是老蒋。上周&#xff0c;老蒋对话标杆直播间第70期邀请到了【华雨婚姻课堂】平台创始人大雨老师&#xff0c;请他聊了聊关于如何有效提高用户粘性&#xff1f;如何深度联动用户&#xff0c;提升高客单转化&#xff1f;也分享了短视频和直播两大赛道关于内…

fpga系列 HDL: 竞争和冒险 02

竞争和冒险 在 Verilog 设计中&#xff0c;竞争&#xff08;race conditions&#xff09;和冒险&#xff08;hazards&#xff09;是数字电路设计中不期望出现的现象&#xff0c;它们会影响电路的正确性。了解并解决竞争和冒险问题对于确保电路稳定运行非常重要。 竞争&#x…