C++:C++11

目录

一.统一的列表初始化

 容器的{}初始化

2. std::initializer_list

std::initializer_list支持迭代器

让模拟实现的vector支持 {} 初始化,只需加上下面这个构造函数:

二.声明

decltype

三.STL中一些变化

 C++里面对victor初始化的时候传参数代表的是将这个victor预先扩容,扩容成指定数字的大小。

四.左值引用和右值引用

1.什么是左值?什么是左值引用?

2.什么是右值?什么是右值引用?

3.左值引用与右值引用比较

(1)左值和右值最大区别:左值可以取地址,右值不能取地址

(2)左值引用总结:

(3)右值引用总结:

五.移动构造与移动赋值

1.将亡值

move, bit: :string s3(move(s1));

2.移动构造 

(1)移动构造介绍

(2)移动构造中的优化

        ① C++98中:

        ② C++11中:加入移动构造

3.移动赋值

4.万能引用,完美转发

5.默认 移动构造函数和移动赋值运算符重载

(1)强制生成默认函数的关键字default:

(2)强制不让生成 delete

六.可变参数模板

1.模板函数包

解释第一个 ShowList(1,'x',1.1) :

 2.emplace_back

七.线程库

1.线程——std::this_ thread

例1:双线程乱序打印线程ip和i值

 例2:隔100ms-0.1s打印一次

2.mutex

例1:在for外面加锁,两线程持续串行打印,一个线程占据for循环时,另一个就一直等待。(是串行打印的)

例2:在for里面加锁,两线程交替串行打印。(是并行打印的)

例3:间接传参的 引用小bug

例4:ref解决引用失效小bug(x指针传参也可以解决)

例5:lambada表达式写入线程初始化的第一个参数

例6:并行++ 和 串行++

3.atomic——CAS操作


一.统一的列表初始化

1.{}初始化

在C++11中,{}的初始化范围增大了,任意类型都可以初始化:内置类型可以用,对于自定义类型,需要提供initializer_list<T>类型的构造函数

 容器的{}初始化

比如vector,list,set,map都支持一个initializer_list类型的构造函数,因此他们都可以用 {} 初始化


	vector<int> v1 = { 1, 2, 3, 4, 5 };
	vector<int> v2 { 1, 2, 3, 4, 5 };
	vector<Date> v3 = { { 2022, 1, 1 }, { 2022, 1, 1 }, { 2022, 1, 1 } };
	list<int> lt1{ 1, 2, 3 };
	set<int> s1{ 3, 4, 5, 6, 3 };
	map<string, string> dict = { { "string", "字符串" }, { "sort", "排序" } };

列表初始化在初始化时,如果出现类型截断,是会报警告或者错误的,所以使用{}初始化和直接初始化还是有区别的。

2. std::initializer_list

std::initializer_list是什么类型:

int main()
{
 // the type of il is an initializer_list 
 auto il = { 10, 20, 30 };
 cout << typeid(il).name() << endl;//class std: initializer list<int>
 return 0; 
}

int main()
{
      vector<int> v = { 1,2,3,4 };
      list<int> lt = { 1,2 };
          // 这里{"sort", "排序"}会先初始化构造一个pair对象
      map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
          // 使用大括号对容器赋值
      v = {10, 20, 30};
      return 0;
}

std::initializer_list支持迭代器

    initializer_list<double> ilt = { 3.3, 5.5, 9.9 };
	initializer_list<double>::iterator it = ilt.begin();
	while (it != ilt.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : ilt)
	{
		cout << e << " ";
	}
	cout << endl;

让模拟实现的vector支持 {} 初始化,只需加上下面这个构造函数:

二.声明

decltype

关键字decltype将变量的类型声明为表达式指定的类型。
即:decltype是推导当前表达式/变量的类型,并用这个类型去定义新的变量;typeid(ret).name() 仅仅是看类型的名字,不能用来定义新变量。
// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2) {
	decltype(t1 * t2) ret;
	cout << typeid(ret).name() << endl;
}
int main()
{
	const int x = 1;
	double y = 2.2;
	decltype(x * y) ret; // 推导出x * y类型是double,所以ret的类型是double
	decltype(&x) p;      // 推导出x类型是int,p的类型是int*
	cout << typeid(ret).name() << endl;
	cout << typeid(p).name() << endl; 
    F(1, 'a');
	return 0;
}

三.STL中一些变化

新容器
用橘色圈起来是 C++11 中的一些几个新容器,但是实际最有用的是 unordered_map
unordered_set 。这两个我们前面已经进行了非常详细的讲解,其他的大家了解一下即可。

 array就是个静态数组,鸡肋

	int a1[10];
	对比c的静态数据,访问更安全
	array<int, 10> a2;

	不一定能检查出来越界:
	//a1[10];

	只要越界一定能检查出来越界:
	//a2[10];

 < forward_ list> 就是单向链表,但是不考虑双链表多开的那一点空间,日常情况下 list就够用了,所以还是鸡肋

 C++里面对victor初始化的时候传参数代表的是将这个victor预先扩容,扩容成指定数字的大小。

四.左值引用和右值引用

1.什么是左值?什么是左值引用?

左值:左值是一个表示数据的表达式(左值不一定是变量,比如解引用的指针*p),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。
特例:定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
左值引用:左值引用就是给左值取别名。
int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

2.什么是右值?什么是右值引用?

右值:右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(传值返回即临时变量)(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能 取地址
右值引用:就是对右值的引用,给右值取别名。
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址,例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地
址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,
这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{
	double x = 1.1, y = 2.2;
以下几个都是常见的右值:
	10; 
    x + y;
	fmin(x, y);
以下几个都是对右值的右值引用:
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
下面编译会报错:error C2106: “=”: 左操作数必须为左值:
	10 = 1; 
    x + y = 1;
	fmin(x, y) = 1;
下面都是错误的,右值不能被引用:
    //cout << &10 << endl;
    //cout << &(x + y) << endl;
    //cout << &fmin(x, y) << endl;
	return 0;
}

3.左值引用与右值引用比较

(1)左值和右值最大区别:左值可以取地址,右值不能取地址

左右值在拷贝上的区别:左值拷贝是不会被资源转移(掠夺)偷家,右值拷贝会被资源掠夺偷家

(2)左值引用总结:

1. 左值引用只能引用左值,不能引用右值。
        void push_ back(T& x)只能传左值,不能传右值。
2. 但是const左值引用既可引用左值,也可引用右值

        void push_ back(const T& x)能传左值和右值。

(3)右值引用总结:

1. 右值引用只能引用右值,不能引用左值。
2. 但是右值引用可以move以后的左值。

int main()
{
	int&& r1 = 10; //右值引用正确写法

	int a = 10;
	int&& r2 = a;  //错误:右值引用不能引用左值
	
	int&& r3 = std::move(a); // 正确,右值引用可以引用move以后的左值
	return 0;
}

五.移动构造与移动赋值

STL的容器,C++11以后,都提供移动构造和移动赋值

1.将亡值

右值分为两种:
1、纯右值(内置类型的右值):10 a+b
2、将亡值(自定义类型的右值):匿名对象string("11111") ,临时对象to_ string(1234), s1+"hello"表达式的返回值 ,move(s1)-> 都是自定义类型对象,这些表达式的返回值就是将亡值

将亡值定义:这些匿名对象的生命周期就在本行,用完就没了。

move, bit: :string s3(move(s1));

【bit: :string s3(move(s1)); move 是用于把左值属性的s1转换出一个临时对象,这个临时对象是一个右值,我们引用这个右值,当其他对象s3拷贝s1时就可以掠夺s1的资源】本身是是不会改变s1的左值属性,仅仅进行右值引用

2.移动构造 

(1)移动构造介绍

移动构造:拷贝将亡值时不能对将亡值深拷贝,应直接把将亡值的资源交给新的对象,就是把将亡值指向资源的地址直接给新的对象(交换_str,_size,_capacity)。而拷贝构造不区分左右值,走的全是深拷贝,效率低。相比之下移动构造效率极高。

swap是自己写的swap,交换了_str,_size,_capacity

namespace bit
{
	class string
	{
	public:

        // 移动构造
		string(string&& s) 右值匹配
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 资源转移" << endl;
			swap(s);    
		}

		// 拷贝构造
		string(const string& s) 左值匹配,实际左右值都能匹配,但右值会去匹配更匹配的
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			string tmp(s._str);
			swap(tmp);
		}
        ……

    int main()
    {
	    bit::string ret = bit::to_string(1234);    1234是右值,调用移动构造
	    cout << ret.c_str() << endl;

        

(2)移动构造中的优化

左值引用的使用场景:
做参数和做返回值都可以提高效率。
左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,
只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,
传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
左右值在拷贝上的区别:左值拷贝是不会被资源转移(掠夺)偷家,右值拷贝会被资源掠夺偷家

        ① C++98中:

str是左值,to_ string(1234)函数中返回str,str因即将出作用域,所以只能先拷贝构造一个临时对象,再用临时对象拷贝构造一个ret对象;编译器优化后str直接拷贝构造ret。(C++98 没有右值引用,所以无法区分是左值还是右值,所以C++98 中的拷贝构造对将亡值也是深拷贝,效率低)

        ② C++11中:加入移动构造

to_ string(1234)函数中返回str,str因即将出作用域,所以只能先拷贝构造一个临时对象,临时对象是将亡值,再用临时对象移动构造一个ret对象;编译器优化后 把返回值str转换识别成右值,然后直接移动构造给ret。(移动构造对直接转移资源,效率极高)

3.移动赋值

namespace bit
{
	class string
	{
	public:

        // 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 资源转移" << endl;
			swap(s);

			return *this;
		}

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}
        ……

    int main()
    {	
	        bit::string ret;
            ret = bit::to_string(1234);
            cout << ret.c_str() << endl;
	        return 0;
    }

4.万能引用,完美转发

模板中的 && 万能引用: T&& t
模板中的 && 不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
我们希望能够在传递过程中保持它的左值或者右值的属性 , 就需要用我们下面学习的完美转发转发
下面解释:
(1)PerfectForward(T&& t) 传参,无论是传左值还是右值,都会被转为左值。但是起码这个右值引用可以都接收 左值和右值了,所以叫 万能引用
(例如:int&& rr1 = 10; 10是右值,rr1是开了一块空间把10存储起来,是可以取地址的,则rr1是左值)
(2)若PerfectForward(int&& t) 不是模板传参,则右值引用不可以接收左值,但是用模板后就可以接收左值和右值,只是接收的左值/右值都会被退化(也叫折叠)为左值。
(3)我们为了让右值传参后还是右值,左值传参后还是左值,就要用到 完美转发
完美转发:copy2 = std::forward<T>(t) 这样可以保持原来的右值/左值属性不变。
(4)copy2 = move(t) 可以吗?——不可以,右值传给t时退化成左值,虽然move可以让左值再转成右值;但是 如果是左值传给t就仍是左值,move会让让左值转成右值,就破坏了左值的属性。
template<typename T>
void PerfectForward(T&& t)
{
	//bit::string copy1 = t;
    完美转发:
    bit::string copy2 = std::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;
}

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; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t) {
    //Fun(t); 原本错的会改变属性的写法
	完美转发:
    Fun(std::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;
}

5.默认 移动构造函数和移动赋值运算符重载

原来C++类中,有6个默认成员函数:

1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数移动赋值运算符重载
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任
意一个。那么编译器会自动生成一个默认移动构造。(析构函数 、拷贝构造、拷贝赋值重载是强绑定的,不用写析构就一定不用写拷贝构造和拷贝赋值重载,不用写其中一个就一定不用写另外两个
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节值拷贝,自定义类型成员,则需要看这个成员是否实现移动构造:如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
的任意一个,那么编译器会自动生成一个默认移动赋值
默认生成的移动赋值,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值:如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
这里就会调用默认移动构造
// 以下代码在vs2013中不能体现,在vs2019下才能演示体现上面的特性。
class Person
{
public:

    Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}

private:
	bit::string _name;
	int _age;
};

int main()
{
	Person s1;
	Person s2 = s1; // 拷贝构造
	Person s3 = std::move(s1); // 移动构造
	Person s4;
	s4 = std::move(s2);

	return 0;
}

(1)强制生成默认函数的关键字default:

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原
因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以
使用default关键字显示指定移动构造生成。

如果我们写了拷贝构造,就不会自动生成默认的移动构造,可以使用default关键字强制移动构造或移动赋值生成。

// 以下代码在vs2013中不能体现,在vs2019下才能演示体现上面的特性。
class Person
{
public:

    Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
    
    Person(const Person& p)
	:_name(p._name)
	,_age(p._age)
	{}
    
    //强制生成移动构造和移动赋值
    Person(Person&& pp) = default;
    Person& operator= ( Person&& pp) = default;

private:
	bit::string _name;
	int _age;
};

int main()
{
	Person s1;
	Person s2 = s1; // 拷贝构造
	Person s3 = std::move(s1); // 移动构造
	Person s4;
	s4 = std::move(s2);

	return 0;
}

(2)强制不让生成 delete

禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁
,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即
可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p) = delete;
private:
	bit::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}

六.可变参数模板

1.模板函数包

Args 是一个模板参数包, args 是一个函数形参参数包

声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。

下面就是一个基本可变参数的函数模板

template <class ...Args>
void ShowList(Args... args)
{}

解释第一个 ShowList(1,'x',1.1) :

1传给val,'x'和1.1传给参数包Args... args,则打印参数包的个数是2个,即sizeof...(args)=2,

cout << sizeof...(args) << endL ;                                   打印:2
cout << val << "->" << typeid(val).name() << endL ;     打印:1->int (val是1,typeid(val).name() 打印val的类型,是int)
ShowL ist(args...);        把剩下的'x'和1.1传入ShowList——第二次传入:

此时'x'传给val,1.1传给参数包Args...args,则打印参数包的个数是1个,即sizeof...(args)=2,

cout << sizeof...(args) << endL ;                                打印:1
cout << val << "->" << typeid(val).name() << endL ;  打印:x->char (val是'x',typeid(val).name() 打印'x'的类型,是char)
ShowL ist(args...);        把仅剩下的1.1传入ShowList——第三次传入:

此时'x'传给val,参数包Args...args什么也不传了,则打印参数包的个数是0个,即sizeof...(args)=0

cout << sizeof...(args) << endL ;                                        打印:0
cout << val << "->" << typeid(val).name() << endL ;          打印:1.1->double(val是1.1,typeid(val).name() 打印1.1的类型,是double)
ShowL ist(args...);        没有数据可以传了,就结束了

 2.emplace_back

template <class... Args>
void emplace_back (Args&&... args);
首先我们看到的 emplace 系列的接口,支持模板的可变参数,并且万能引用。那么相对 insert
emplace 系列接口的优势到底在哪里呢?
int main()
{
	std::list< std::pair<int, char> > mylist;
	// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
	// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
	mylist.emplace_back(10, 'a');
	mylist.emplace_back(20, 'b');
	mylist.emplace_back(make_pair(30, 'c'));
	mylist.push_back(make_pair(40, 'd'));
	mylist.push_back({ 50, 'e' });
	for (auto e : mylist)
		cout << e.first << ":" << e.second << endl;
	return 0;
}

int main()
{
	// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
	// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
	// 是先构造,再移动构造,其实也还好。
	std::list< std::pair<int, bit::string> > mylist;
	mylist.emplace_back(10, "sort");
	mylist.emplace_back(make_pair(20, "sort"));
	mylist.push_back(make_pair(30, "sort"));
	mylist.push_back({ 40, "sort" });
	return 0;
}

emplace_back相比push_ back优化:对于list中尾插右值pair,

mylist.emplace_back(10, "sort"); 和mylist.emplace_back(make_pair(20, "sort"));是直接构造了,mylist.push_back(make_pair(30, "sort")); 和 mylist.push_back({ 40, "sort" });是先构造,再移动构造,多了一次移动构造,其实也还好,因为 push_back:拷贝构造(深拷贝)+移动构造(资源转移) 和 emplace_back:直接一次拷贝构造(深拷贝) 的效率差别不大。但是对于Date类这种不需要深拷贝的类型的更明显,因为日期类是浅拷贝,没有资源转移,所以日期类的移动构造就变成的浅拷贝,push_back:拷贝构造(浅拷贝)+移动构造(浅拷贝) 和 emplace_back:直接一次拷贝构造(浅拷贝),emplace_back是直接构造效率会更高。

七.线程库

thread 类的简单介绍
C++11 之前,涉及到多线程问题,都是和平台相关的,比如 windows linux 下各有自己的接
口,这使得代码的可移植性比较差 C++11 中最重要的特性就是对线程进行支持了,使得 C++
并行编程时不需要依赖第三方库 ,而且在原子操作中还引入了原子类的概念。要使用标准库中的
线程,必须包含 < thread > 头文件。 C++11 中线程类
注意:
1. 线程是操作系统中的一个概念, 线程对象可以关联一个线程,用来控制线程以及获取线程的
状态
2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
get_id() 的返回值类型为 id 类型, id 类型实际为 std::thread 命名空间下封装的一个类,该类中
包含了一个结构体:

fn万能引用传函数,Args模板可变参数 是先传给thread构造函数,然后通过构造函数间接拷贝方式传给这个fn函数。

1.线程——std::this_ thread

get_id 得到线程id

例1:双线程乱序打印线程ip和i值

 例2:隔100ms-0.1s打印一次

2.mutex

例1:在for外面加锁,两线程持续串行打印,一个线程占据for循环时,另一个就一直等待。(是串行打印的)

例2:在for里面加锁,两线程交替串行打印。(是并行打印的)

例3:间接传参的 引用小bug

fn万能引用传函数,因为Args模板可变参数 是先传给thread构造函数,然后通过构造函数间接拷贝方式传给这个fn函数。所以引用不起作用,也是编译器识别的bug。即:这无论是int x和int& x,都是拷贝传参。

例4:ref解决引用失效小bug(x指针传参也可以解决)

ref+全局变量mutex

ref作用:强制以左值引用传参,这样引用就可以起作用了。(如果上面是int x就还是传值拷贝)

ref+mutex 传参, ref(count), ref(m)

#include<iostream>
#include<thread>
#include<mutex>
#include <condition_variable>
#include<vector>
#include<atomic>
using namespace std;

void Print(int n, int& x, mutex& mtx)
{
	for (int i = 0; i < n; ++i)
	{
		mtx.lock();

		cout <<this_thread::get_id()<<":"<<i << endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
		++x;

		mtx.unlock();
	}

}

int main()
{
	mutex m;
	int count = 0;
	thread t1(Print, 10, ref(count), ref(m));
	thread t2(Print, 10, ref(count), ref(m));

	t1.join();
	t2.join();

	cout << count << endl;

	return 0;
}

例5:lambada表达式写入线程初始化的第一个参数

#include<iostream>
#include<thread>
#include<mutex>
#include<vector>
int main()
{
	mutex mtx;
	int x = 0;
	int n = 10;
	int m;
	cin >> m;

	vector<thread> v(m);
	//v.resize(m);

	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){        //&捕捉所有变量
			for (int i = 0; i < n; ++i)
			{
				mtx.lock();

				cout << this_thread::get_id() << ":" << i << endl;
				std::this_thread::sleep_for(std::chrono::milliseconds(100));
				++x;

				mtx.unlock();
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}

例6:并行++ 和 串行++

并行++比串行++慢,因为申请锁、释放锁要花时间。并行++要400w次申请释放锁,串行++只需要4次。


int main()
{
	mutex mtx;
	int x = 0;
	int n = 1000000;
	int m;
	cin >> m;

	vector<thread> v(m);
	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				// 并行
				mtx.lock();
				++x;
				mtx.unlock();
			}
		});

		//v[i] = thread([&](){
		//	// 串行
		//	mtx.lock();
		//	for (int i = 0; i < n; ++i)
		//	{
		//		++x;
		//	}
		//	mtx.unlock();
		//});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}

3.atomic——CAS操作

CAS 操作(compare and swap):当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。

举例说明:atomic<int> x = 0;  则++x时不用加锁了,4个线程可以任意++,若出现同时++的情况,假如x=0,线程t1、t2、t3、t4同时++,t1先把x=1放回内存时,此时内存值是0,比较0和1,发现未被改变,则可以把自己++后的1放入内存;然后t2 / t3 / t4再想放入自己的++结果1时,compare比较当前内存值和之前取得的值是否相等,相等则内存中数据已被修改,则写入失败,重新执行++并尝试写入。

int main()
{
	mutex mtx;
	atomic<int> x = 0;
	//int x = 0;
	int n = 1000000;
	int m;
	cin >> m;

	vector<thread> v(m);
	for (int i = 0; i < m; ++i)
	{
		// 移动赋值给vector中线程对象
		v[i] = thread([&](){
			for (int i = 0; i < n; ++i)
			{
				// t1 t2 t3 t4
				++x;
			}
		});
	}

	for (auto& t : v)
	{
		t.join();
	}

	cout << x << endl;

	return 0;
}

两个线程交错打印1-100,一个打印奇数,一个打印偶数


int main()
{
	int i = 0;
	int n = 100;

	thread t1([&](){
		while (i < n)
		{
			while (i % 2 != 0)
			{
				this_thread::yield();
			}

			cout <<this_thread::get_id()<<":"<<i << endl;
			i += 1;
		}
	});


	thread t2([&](){
		while(i < n)
		{
			while (i % 2 == 0)
			{
				this_thread::yield();
			}

			cout << this_thread::get_id() << ":" << i << endl;
			i += 1;
		}
	});

	t1.join();
	t2.join();

	return 0;
}

t2打印偶数,所以让线程2先走

 

当pred是false时,需要wait 

int main()
{
	int i = 0;
	int n = 100;
	mutex mtx;
	condition_variable cv;
	bool ready = true;

	// t1打印奇数
	thread t1([&](){
		while (i < n)
		{
			{
				unique_lock<mutex> lock(mtx);
				cv.wait(lock, [&ready](){return !ready; });

				cout << "t1--" << this_thread::get_id() << ":" << i << endl;
				i += 1;

				ready = true;

				cv.notify_one();
			}

			//this_thread::yield();
			this_thread::sleep_for(chrono::microseconds(100));
		}
	});

	// t2打印偶数
	thread t2([&]() {
		while (i < n)
		{
			unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&ready](){return ready; });

			cout <<"t2--"<<this_thread::get_id() << ":" << i << endl;
			i += 1;
			ready = false;

			cv.notify_one();
		}
	});

	this_thread::sleep_for(chrono::seconds(3));

	cout << "t1:" << t1.get_id() << endl;
	cout << "t2:" << t2.get_id() << endl;

	t1.join();
	t2.join();

	return 0;
}

猜你喜欢

转载自blog.csdn.net/zhang_si_hang/article/details/126986192