【C++】C++多线程数据共享问题、互斥量、死锁及解决方法

目录

一.线程间数据共享的问题

二.保护共享数据的方法

三.死锁

四.死锁的解决方案

五.std::unique_lock

六.锁的粒度

七.unique_lock所有权的传递


一.线程间数据共享的问题

问题:你和你的朋友合租一个公寓, 公寓中只有一个厨房和一个卫生间。 当你的朋友在卫生间时, 你就会不能使用了;或者在购票网站上两个人订票,同一个座位,当一行订票操作时,另一个人就不能再操作或者即使操作也不会成功,否则就会出很多的麻烦。

同样的问题,对于多线程来说,有以下几种情况:

1.当两个线程访问不同的内存位置时,不会存在问题,相当于你和朋友不合租,各住各的;

2.两个线程对共享数据进行操作时,如果只是一起读取,不会出什么问题;

3.但是一个线程要读取,另一个线程要写入,就会出现问题,所以保护共享数据是需要在多线程中考虑的。

以下代码未考虑读数据线程和写数据线程之间的数据共享问题,会导致刚刚分析过的问题而报错。

#include <iostream>
#include <thread>
#include <list>
using namespace std;

class OperateData{
public:
	//写入数据
	void writeData()
	{
		for (int i = 0; i < 10000; i++)
		{
			cout << "写入数据" << i << endl;
			dataList.push_back(i);
		}
	}
	//读取数据
	void readData()
	{
		for (int i = 0; i < 10000; i++)
		{
			if (!dataList.empty())//数据不为空
			{
				int data = dataList.front();//返回第一个元素,但不检查元素是否存在
				dataList.pop_front();//移除第一个元素,但不返回
			}
			else
			{
				//数据为空
				cout << "数据为空"<<endl;
			}
		}
		cout << "end" << endl;
	}
private:
	list<int> dataList;
};
int main()
{
	OperateData myobj;
	thread readObj(&OperateData::readData,&myobj);//第二个参数是引用才能保证线程里用的是同一个对象
	thread writeObj(&OperateData::writeData, &myobj);
	readObj.join();
	writeObj.join();
}

二.保护共享数据的方法

使用互斥量(mutex)保护共享数据

当访问共享数据前, 将数据锁住, 在访问结束后, 再将数据解锁。 线程库需要保证, 当一个线程使用特定互斥量锁住共享数据时, 其他的线程想要访问锁住的数据, 都必须等到之前那个线程对数据进行解锁后, 才能进行访问。

互斥量自身也有问题, 也会造成死锁, 或对数据保护的太多(或太少)。

首先需要引入头文件:

#include <mutex>

C++中通过实例化 std::mutex 创建互斥量实例, 通过成员函数lock()对互斥量上锁, unlock()
进行解锁。也就是:

lock()-->操作共享数据-->unlock()

lock()和unlock()一定要成对使用!

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;

class OperateData{
public:
	//写入数据
	void writeData()
	{
		for (int i = 0; i < 100000; i++)
		{
			cout << "写入数据" << i << endl;
			my_mutex.lock();
			dataList.push_back(i);
			my_mutex.unlock();
		}
	}
	//读取数据
	void readData()
	{
		for (int i = 0; i < 100000; i++)
		{
			my_mutex.lock();
			if (!dataList.empty())//数据不为空
			{
				int data = dataList.front();//返回第一个元素,但不检查元素是否存在
				dataList.pop_front();//移除第一个元素,但不返回
			}
			else
			{
				//数据为空
				cout << "数据为空"<<endl;
			}
			my_mutex.unlock();
		}
		cout << "end" << endl;
	}
private:
	list<int> dataList;
	mutex my_mutex;
};
int main()
{
	OperateData myobj;
	thread readObj(&OperateData::readData,&myobj);//第二个参数是引用才能保证线程里用的是同一个对象
	thread writeObj(&OperateData::writeData, &myobj);
	readObj.join();
	writeObj.join();

	return 0;
}

修改后的程序就可以稳定运行了。

由于成对使用时容易遗漏,C++标准库为互斥量提供了一个模板类 std::lock_guard ,在构造时就能提供已锁(执行lock)的互斥量, 并在析构的时候进行解锁(执行unlock), 从而保证了一个已锁互斥量能被正确解锁。使用了lock_guard就不能使用lock()和unlock()了。

std::lock_guard<std::mutex> guard(my_mutex);
//写入数据
	void writeData()
	{
		for (int i = 0; i < 100000; i++)
		{
			cout << "写入数据" << i << endl;
			lock_guard<std::mutex> guard(my_mutex);
			dataList.push_back(i);
		}
	}
	//读取数据
	void readData()
	{
		for (int i = 0; i < 100000; i++)
		{
			lock_guard<std::mutex> guard(my_mutex);
			if (!dataList.empty())//数据不为空
			{
				int data = dataList.front();//返回第一个元素,但不检查元素是否存在
				dataList.pop_front();//移除第一个元素,但不返回
			}
			else
			{
				//数据为空
				cout << "数据为空"<<endl;
			}
		}
		cout << "end" << endl;
	}

三.死锁

一对线程需要对他们所有的互斥量做一些操作, 其中每个线程都有一个互斥量, 且等待另一个解锁。 这样没有线程能工作, 因为他们都在等待对方释放互斥量。 这种情况就是死锁, 它的最大问题就是由两个或两个以上的互斥量来锁定一个操作。

死锁产生的前提条件是至少有两个互斥量

两个线程A,B

(1)线程A执行,互斥量A锁定,接下来轮到互斥量B锁定

(2)这时恰好出现上下文切换        

(3)线程B执行,互斥量B锁定,接下来轮到互斥量A锁定,然后...发现A已经锁了

(4)这时就出现了死锁

出现互锁情况的代码:

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;

class OperateData{
public:
	//写入数据
	void writeData()
	{
		for (int i = 0; i < 100000; i++)
		{
			cout << "写入数据" << i << endl;
			my_mutex_1.lock(); //先锁1后锁2
            my_mutex_2.lock();
			dataList.push_back(i);
			my_mutex_2.unlock();
            my_mutex_1.unlock(); //解锁的顺序无所谓
		}
	}
	//读取数据
	void readData()
	{
		for (int i = 0; i < 100000; i++)
		{
			my_mutex_2.lock(); //先锁2后锁1
            my_mutex_1.lock();
			if (!dataList.empty())//数据不为空
			{
				int data = dataList.front();//返回第一个元素,但不检查元素是否存在
				dataList.pop_front();//移除第一个元素,但不返回
			}
			else
			{
				//数据为空
				cout << "数据为空"<<endl;
			}
			my_mutex_2.unlock();
            my_mutex_1.unlock(); //解锁的顺序无所谓
		}
		cout << "end" << endl;
	}
private:
	list<int> dataList;
	mutex my_mutex_1;
	mutex my_mutex_2;
};
int main()
{
	OperateData myobj;
	thread readObj(&OperateData::readData,&myobj);//第二个参数是引用才能保证线程里用的是同一个对象
	thread writeObj(&OperateData::writeData, &myobj);
	readObj.join();
	writeObj.join();

	return 0;
}

四.死锁的解决方案

1.只要保持两个互斥量上锁的顺序一致就不会死锁,lock_guard也是一样的;

std::lock_guard<std::mutex> guard(my_mutex_1);

std::lock_guard<std::mutex> guard(my_mutex_2);

2.上述也存在死锁风险的情况,C++标准库有办法解决这个问题, std::lock() ——可以一次性锁住多个(两个以上)的互斥量, 并且没有副作用(死锁风险)。当 std::lock 成功的获取一个互斥量上的锁, 并且当其尝试从另一个互斥量上再获取锁时, 就会有异常抛出,第一个锁也会随着异常的产生而自动释放, 所以 std::lock 要么将两个锁都锁住, 要不一个都不锁。

std::lock(my_mutex_1,my_mutex_2);

//....

my_mutex_1.unlock();

my_mutex_2.unlock();

3.使用std::lock()很容易忘记使用unlock(),针对这种情况,C++17对这种情况提供了支持,std::scoped_lock<> 一种新的RAII类型模板类型,与 std::lock_guard<> 的功能等价, 这个新类型能接受不定数量的互斥量类型作为模板参数,以及相应的互斥量(数量和类型)作为构造参数。 互斥量支持构造即上锁, 与 std::lock 的用法相同, 其解锁阶段是在析构中进行。 

std::scoped_lock<std::mutex,std::mutex> guard(lhs.m,rhs.m);

4.同样的,也可以使用std::lock()和std::lock_guard<>配合使用,但是std::lock_guard<>已经在构造中默认使用的了std::lock(),所以要在参数中设置std::adopt_lock避免调用构造中的lock()。

std::lock(my_mutex_1,my_mutex_2);

//....

std::lock_guard<std::mutex> guard_1(my_mutex_1,std::adopt_lock);

std::lock_guard<std::mutex> guard_2(my_mutex_2,std::adopt_lock);

5.避免嵌套锁:一个线程已获得一个锁时, 再别去获取第二个。 因为每个线程只持有一个锁, 锁上就不会产生死锁。

五.std::unique_lock

std::unique_lock与std::lock_guard<> 的功能类似,在没有参数时可以替换使用,但是std::unique_lock更灵活,可以通过更多的参数去适配,但std::unique_lock 会占用比较多的空间, 并且比 std::lock_guard 稍慢一些。

1.参数std::adopt_lock

和上述在std::lock_guard<>中使用一样,需要std::lock()和std::lock_guard<>配合使用

std::lock(my_mutex_1,my_mutex_2);

//....

std::unique_lock<std::mutex> guard_1(my_mutex_1,std::adopt_lock);

std::unique_lock<std::mutex> guard_2(my_mutex_2,std::adopt_lock);

2.参数std::try_to_lock

不能与lock同时使用,尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里;使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方。通过owns_lock()判断是否拿到锁。

std::unique_lock<std::mutex> guard(my_mutex,std::try_to_lock);
if(guard.owns_lock())
{
    //拿到了锁
    dataList.push_back(i);
}
else
{
    //没拿到锁
    cout<<"没拿到锁"<<endl;
}

3.参数std::defer_lock

初始化了一个没有加锁的mutex,不给它加锁的目的是以后可以调用unique_lock的一些方法,不能提前lock。

4.std::unique_lock成员函数

  • lock()
std::unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock(); //自动unlock()
  • unlock()
std::unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
//...处理共享数据
myUniLock.unlock(); //暂时解开
//...处理非共享数据
myUniLock.lock();
  • try_lock()
std::unique_lock<std::mutex> guard(my_mutex,std::defer_lock);
if(guard.try_lock() == true)
{
    //拿到了锁
    dataList.push_back(i);
}
else
{
    //没拿到锁
    cout<<"没拿到锁"<<endl;
}
  • release()

std::unique_lock<mutex> myUniLock(myMutex);相当于把myMutex和myUniLock绑定在了一起,release()就是解除绑定,返回它所管理的mutex对象的指针,并释放所有权。

std::unique_lock<std::mutex> myUniLock(my_mutex);
std::mutex* ptx = myUniLock.release();//解绑my_mutex和myUniLock
//...操作共享数据
ptx->unlock(); //需要自己手动解锁

六.锁的粒度

锁的粒度是用来描述通过一个锁保护着的数据量大小。 一个细粒度锁(a fine-grained lock)能够保护较小的数据量, 一个粗粒度锁(a coarse-grained lock)能够保护较多的数据量。要选择合适粒度的锁。

七.unique_lock所有权的传递

复制所有权是非法的

std::unique_lock<std:mutex> myUniLock_1(mutex);
std::unique_lock<std:mutex> myUniLock_2(myUniLock_1);

 需要移动所有权

std::unique_lock<std:mutex> myUniLock_1(mutex);
std::unique_lock<std:mutex> myUniLock_2(std::move(myUniLock_1)); //现在myUniLock_1指向空,myUniLock_2指向mutex
std::unique_lock<std:mutex> lk()
{
    std::unique_lock<std:mutex> tempUniLock(mutex);
    return tempUniLock;//移动构造函数
}
// 然后就可以在外层调用,在guard具有对mutex的所有权
std::unique_lock<std::mutex> guard = lk();

  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

菜鸟赵大宝

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

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

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

打赏作者

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

抵扣说明:

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

余额充值