系统的学习一下C++标准的多线程----在线程间共享数据

目录

线程共享数据

竞争条件

用互斥元保护共享数据

使用C++中的互斥元

使用互斥元上锁以及解锁

结果:

使用模板类上锁 

结果:

 为保护共享数据精心组织代码

避免死锁的进一步指南

在std::unique_lock灵活锁定

锁定在恰当的粒度



线程共享数据

竞争条件

将线程用作并行的关键优点之一,是在他们之间简单直接地共享数据的潜力。

从整体来看,所有线程之间共享数据的问题,都是修改数据导致的。如果所有的共享数据都是只读的,就没有问题,因为一个线程所读取的数据不受另外一个线程是否正在读取相同的数据二影响。

一个被广泛用来帮助程序员推到代码的概念,就是不变量。

并发代码中错误的最常见的诱因之一:竞争条件。

用互斥元保护共享数据

使用C++中的互斥元

如果你可以将所有访问该数据结构的代码标记为互斥,其中任何一个线程访问必须等到正在访问的线程完成才可以访问。

在访问共享数据结构之前锁定(lock)与该数据相关的互斥元,当访问数据结构完成后,解锁(unlock)该互斥元。线程库会确保一旦一个线程已经锁定某个互斥元,所有其他试图锁定相同的互斥元的线程必须等待,直到成功锁定了该互斥元的线程解锁此互斥元。

互斥元也有自己的问题,死锁和保护过多或过少数据。

在C++中通过构造std::mutex的实例来创建互斥元。调用lock()函数来锁定它,调用成员函数unlock()来解锁它。然而不推荐这么做,因为这意味着你必须记住离开函数的每条代码路径上都调用unlock(),包括由于异常所导致的在内。

作为替代,C++提供std::lock_guard类模板,它在构造时将互斥元上锁,在析构时将互斥元解锁,从而保证被锁定的互斥元始终被正确解锁。

这两个都在头文件<mutex>中。

未使用互斥元的代码:

当两个线程同时堆一个数增加,理论上因该时2‘000’000次但是实际上会有差别,本次的测试是

#include <iostream>
#include <thread>
using namespace std;
int n = 0;
void func() {
	for (int i = 1; i <= 1'000'000; i++)
		n++;
}
int main() {
	thread t1(func);
	thread t2([]() {
		func();
	});
	if (t1.joinable())
	{
		t1.join();
	}
	if (t2.joinable())
	{
		t2.join();
	}
	cout << n << endl;
	return 0;
}

使用互斥元上锁以及解锁

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int n = 0;
mutex mtx;
void func() {
	
	for (int i = 1; i <= 1'000'000; i++)
	{
		mtx.lock();
		n++;
		mtx.unlock();
	}
		
}
int main() {
	thread t1(func);
	thread t2([]() {
		func();
	});
	if (t1.joinable())
	{
		t1.join();
	}
	if (t2.joinable())
	{
		t2.join();
	}
	cout << n << endl;
	return 0;
}

结果:

当然这种方式极有好处也有坏处,好处就是比较直观容易理解,但是坏处也很明显,就是容易发生死锁。(以后讨论)以及代码出现异常无法解锁等问题。

使用模板类上锁 

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int n = 0;
mutex mtx;
void func() {
	
	for (int i = 1; i <= 1'000'000; i++)
	{
		lock_guard<mutex> l(mtx);
		n++;
		
	}
		
}
int main() {
	thread t1(func);
	thread t2([]() {
		func();
	});
	if (t1.joinable())
	{
		t1.join();
	}
	if (t2.joinable())
	{
		t2.join();
	}
	cout << n << endl;
	return 0;
}

结果:

 为保护共享数据精心组织代码

注意:不要将受保护数据的指针和引用传递到锁范围之外,无论是通过函数返回还是将其存放在外面可见的内存中,还是作为参数传递给用户提供的函数。

避免死锁的进一步指南

  • 避免嵌套锁
  • 在持有锁时,避免调用用户提供的代码
  • 以固有顺序获取锁

在std::unique_lock灵活锁定

 std::unique_lock为锁管理模板类,是对通用mutex的封装。std::unique_lock对象以独占所有权的方式(unique owership)管理mutex对象的上锁和解锁操作,即在unique_lock对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而unique_lock的生命周期结束之后,它所管理的锁对象会被解锁。unique_lock具有lock_guard的所有功能,而且更为灵活。虽然二者的对象都不能复制,但是unique_lock可以移动(movable),因此用unique_lock管理互斥对象,可以作为函数的返回值,也可以放到STL的容器中。

 关于std::mutex的基础介绍可以参考:http://blog.csdn.net/fengbingchun/article/details/73521630  

 std::unique_lock还支持同时锁定多个mutex,这避免了多道加锁时的资源”死锁”问题。在使用std::condition_variable时需要使用std::unique_lock而不应该使用std::lock_guard。

 std::unique_lock类成员函数介绍:

  1.   unique_lock构造函数:禁止拷贝构造,允许移动构造;
  2.   operator =:赋值操作符,允许移动赋值,禁止拷贝赋值;
  3.   operator bool:返回当前std::unique_lock对象是否获得了锁;
  4.   lock函数:调用所管理的mutex对象的lock函数;
  5.   try_lock函数:调用所管理的mutex对象的try_lock函数;
  6.  try_lock_for函数:调用所管理的mutex对象的try_lock_for函数;
  7.  try_lock_until函数:调用所管理的mutex对象的try_lock_until函数;
  8.   unlock函数:调用所管理的mutex对象的unlock函数;
  9.   release函数:返回所管理的mutex对象的指针,并释放所有权,但不改变mutex对象的状态;
  10.  (10). owns_lock函数:返回当前std::unique_lock对象是否获得了锁;
  11.  (11). mutex函数:返回当前std::unique_lock对象所管理的mutex对象的指针;
  12.  (12). swap函数:交换两个unique_lock对象。

这篇博客写的很清楚参考

C++11中std::unique_lock的使用_网络资源是无限的-CSDN博客_std::unique_lock

锁定在恰当的粒度

锁粒度是一个文字术语,用来描述由单个锁保护的数据量。细锁粒度保护着少量的数据,粗锁粒度保护着大量的数据。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

波雅_汉库克

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值