iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C++多线程之互斥锁与死锁
  • 732
分享到

C++多线程之互斥锁与死锁

2024-04-02 19:04:59 732人浏览 薄情痞子
摘要

目录1.前言2.互斥锁2.1 互斥锁的特点2.2 互斥锁的使用2.3 std::lock_guard3.死锁3.1 死锁的含义3.2 死锁的例子3.3 死锁的解决方法1.前言 比如说

1.前言

比如说我们现在以一个list容器来模仿一个消息队列,当消息来临时插入list的尾部,当读取消息时就把头部的消息读出来并且删除这条消息。在代码中就以两个线程分别实现消息写入和消息读取的功能,如下:


class msgList
{
private:
	list<int>mylist;   //用list模仿一个消息队列
 
public:
	void WriteList()   //向消息队列中写入消息(以i作为消息)
	{
		for (int i = 0; i<100000; i++)
		{
			cout << "Write : " << i <<endl;
			mylist.push_back(i);
		}
		return;
	}
	void ReadList()  //从消息队列中读取并取出消息
	{
		for(int i=0;i<100000;i++)
		{
			if (!mylist.empty())
			{
				cout << "Read : " << mylist.front() << endl;
				mylist.pop_front();
			}
			else
			{
				cout << "Message List is empty!" << endl;
			}
		}
	}
};
int main()
{
	msgList mlist;
	thread pread(&msgList::ReadList, &mlist);   //读线程
	thread pwrite(&msgList::WriteList, &mlist);   //写线程
     //等待线程结束
	pread.join(); 
	pwrite.join();
 
    return 0;
}

这段程序在运行过程中,大部分时间是正常的,但是也会出现如下不稳定的情况:

为什么会出现这种情况呢?

这是因为消息队列对于读线程和写线程来说是共享的,这时就会出现两种特殊的情况:读线程的读取操作还没有结束,线程上下文就切换到了写线程中;或者写线程的写入操作还没有结束,线程上下文切换就到了读线程中,这两种情况都反映了读写冲突,从而出现了以上错误。

要想解决这个问题,最显然最直接的方法就是将读写操作分离开来,读的时候不允许写,写的时候不允许读,这样,才能实现线程安全的读和写。说形象一点,就是在进行读操作时,就对共享资源进行加锁,禁止其他线程访问,其他线程要访问就得等到读线程解锁才行,就像上厕所一样,一次只能上一个人,其他人必须得等他上完了再上。这样,就有了互斥锁的概念。

2.互斥锁

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。比如说,同一个文件,可能一个线程会对其进行写操作,而另一个线程需要对这个文件进行读操作,可想而知,如果写线程还没有写结束,而此时读线程开始了,或者读线程还没有读结束而写线程开始了,那么最终的结果显然会是混乱的。为了保护共享资源,在线程里也有这么一把锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。

2.1 互斥锁的特点

1. 原子性:把一个互斥量锁定为一个原子操作,这意味着如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;

2. 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;

3. 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。

2.2 互斥锁的使用

根据前面我们可以知道,互斥锁主要就是用来保护共享资源的,在c++ 11中,互斥锁封装在mutex类中,通过调用类成员函数lock()和unlock()来实现加锁和解锁。值得注意的是,加锁和解锁,必须成对使用,这也是比较好理解的。除此之外,互斥量的使用时机,就以开篇程序为例,我们要保护的共享资源当然就是消息队列list了,那么互斥锁应该加在哪里呢?

可能想的比较简单一点:就直接把锁加在函数最前面不就好了么?如下所示:


class msgList
{
private:
	list<int>mylist;   //用list模仿一个消息队列
        mutex mtx;   //创建互斥锁对象
public:
	void WriteList()   //向消息队列中写入消息(以i作为消息)
	{
                mtx.lock();
		for (int i = 0; i<100000; i++)
		{
			cout << "Write : " << i <<endl;
			mylist.push_back(i);
		}
                mtx.unlock();
		return;
	}
	//.......
};

不过如果这样加锁的话,要等写线程完全执行结束才能开始读线程,读写线程变成了串行执行,这就违背了线程并发性的特点了。正确的加锁方式应当是在执行写操作的具体部分加锁,如下所示:


class msgList
{
private:
	list<int>mylist;   //用list模仿一个消息队列
        mutex mtx;   //创建互斥锁对象
public:
	void WriteList()   //向消息队列中写入消息(以i作为消息)
	{   
		for (int i = 0; i<100000; i++)
		{
                        mtx.lock();
			cout << "Write : " << i <<endl;
			mylist.push_back(i);
                        mtx.unlock();
		}
		return;
	}
	//.......
};

这样,才能真正的实现读写互不干扰。

下面再举一个更为直观的例子,创建两个线程同时对list进行写操作:


class msgList
{
private:
	list<int>mylist;
	mutex m;
	int i = 0;
public:
	void WriteList()
	{
		while(i<1000)
		{
			mylist.push_back(i++);
		}
		return;
	}
	void showList()
	{
		for (auto p = mylist.begin(); p != mylist.end(); p++)   
		{
			cout << (*p) << " ";
		}
		cout << endl;
		cout << "size of list : " << mylist.size() << endl;
		return;
	}
};
int main()
{
	msgList mlist;
	thread pwrite0(&msgList::WriteList, &mlist);
	thread pwrite1(&msgList::WriteList, &mlist);
 
	pwrite0.join();
	pwrite1.join();
	cout << "threads end!" << endl;
 
	mlist.showList();  //子线程结束后主线程打印list
    return 0;
}

这里用两个线程来写list,并且最终在主线程中调用了showList()来输出list的size和所有元素,我们先来看下输出情况:

根据结果可以看到,这里有很多问题:实际输出的元素个数和size不符,输出的元素也并不是连续的,这都是多个线程同时更新list所造成的情况。这种情况下,运行结果是无法预料的,每次都可能不一样。这就是线程不安全所引发的问题,我们加上锁再来看看:


class msgList
{
private:
	list<int>mylist;
	mutex m;
	int i = 0;
public:
	void WriteList()
	{
		while(i<1000)
		{
                        m.lock();//加锁
			mylist.push_back(i++);
                        m.unlock(); //解锁
		}
		return;
	}
	// ......
};

这样加锁就正确了吗?我们再多运行几次看看:

数字都是连续的,但是个数却多了一个(出现的几率还是比较小),这又是什么原因造成的呢?还是两个线程的问题,假设要插入1000个数,循环条件就是while(i<1000),当i=999的时候两个写线程都可以进入while循环,此时如果pwrite0线程拿到了lock(),那么pwrite1线程就只能一直等待,pwrite0线程继续往下执行,使得i变成了1000,此时,对于pwrite0线程来说,它就必须退出循环了。而此时的pwrite1在哪里呢?还等在lock()的地方,pwrite0线程unlock()后,pwrite1成功lock(),此时i=1000,但是pwrite1却还没有执行完此次循环,因此向list中插入1000,此时退出的i的值为1001,这也就造成了实际输出为1001个数的情况。

为了避免这个问题,一个简单的办法就是在lock()之后再加上一个判断,判断i是否依旧满足while的条件,如下:


void WriteList()
	{
		while(i<10000)
		{
			m.lock();
			if (i >= 10000)
			{
				m.unlock();   //退出之前必须先解锁
				break;
			}
			mylist.push_back(i++);
			m.unlock();
		}
		return;
	}

为什么这里要在break前面加一个unlock()呢?原因就在于:如果break前面没有unlock(),一旦i符合了if的条件,就直接break了,此时就没法unlock(),程序就会报错:

可以发现,这种错误是比较难发现的,特别是像这样程序中出现了分支的情况,很容易就使得程序实际运行时lock()了却没有unclock()。为了解决这一问题,就有了std::lock_guard。

2.3 std::lock_guard

简单来理解的话,lock_guard就是一个类,它会在其构造函数中加锁,而在析构函数中解锁,也就是说,只要创建一个lock_guard的对象,就相当于lock()了,而该对象析构时,就自动调用unlock()了。

就以上述程序为例,直接改写为:


void WriteList()
	{
		while(i<10000)
		{
                        lock_guard<mutex> guard(m);  //创建lock_guard的类对象guard,用互斥量m来构造
			//m.lock();   
			if (i >= 10000)
			{
				//m.unlock();   //由于有了guard,这里就无需unlock()了
				break;
			}
			mylist.push_back(i++);
			//m.unlock();
		}
		return;
	}

这里主要有两个需要注意的地方:第一、原先的lock()和unlock()都不用了;第二、if中的break前面也不用再调用unlock()了。这都是因为对象guard在lock_guard一句处构造出来,同时就调用了lock(),当退出while时,guard析构,析构时就调用了unlock()。(局部对象的生命周期就是创建该对象时离其最近的大括号的范围{})

3.死锁

3.1 死锁的含义

死锁是什么意思呢?举个例子,我和你手里都拽着对方家门的钥匙,我说:“你不把我的锁还来,我就不把你的锁给你!”,你一听不乐意了,也说:“你不把我的锁还来,我也不把你的锁给你!”就这样,我们两个人互相拿着对方的锁又等着对方先把锁拿来,然后就只能一直等着等着等着......最终谁也拿不到自己的锁,这就是死锁。

显然,死锁是发生在至少两个锁之间的,也就是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行,当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

3.2 死锁的例子


mutex m0,m1;
int i = 0;
void fun0()
{
	while (i < 100)
	{
		lock_guard<mutex> g0(m0);  //线程0加锁0
		lock_guard<mutex> g1(m1);  //线程0加锁1
		cout << "thread 0 running..." << endl;
	}
	return;
}
void fun1()
{
	while (i < 100)
	{
		lock_guard<mutex> g1(m1);  //线程1加锁1
		lock_guard<mutex> g0(m0);  //线程1加锁0
		cout << "thread 1 running...   "<< i << endl;
	}
	return;
}
int main()
{
	thread p0(fun0);
	thread p1(fun1);
	p0.join();
	p1.join();
    return 0;
}

我们来看下运行结果:

这就出现了死锁。产生的原因就是因为在线程0中,先加锁0,再加锁1;在线程1中,先加锁1,再加锁0;如果两个线程之一能够完整执行的话,那自然是没有问题的,但是如果某个时刻,线程0中刚加锁0,就上下文切换到线程1,此时线程1就加锁1,然后此时两个线程都想向下执行的话,线程1就必须等待线程0解锁0,线程0就必须等待线程1解锁1,就这样两个线程都一直阻塞着,形成了死锁。

3.3 死锁的解决方法

①按顺序加锁

以上述例程来说,就是线程0和线程1的加锁顺序保持一致,如下所示:


mutex m0,m1;
int i = 0;
void fun0()
{
	while (i < 100)
	{
		lock_guard<mutex> g0(m0);  //线程0加锁0
		lock_guard<mutex> g1(m1);  //线程0加锁1
		cout << "thread 0 running..." << endl;
	}
	return;
}
void fun1()
{
	while (i < 100)
	{
                lock_guard<mutex> g0(m0);  //线程1加锁0
		lock_guard<mutex> g1(m1);  //线程1加锁1
		cout << "thread 1 running...   "<< i << endl;
	}
	return;
}
int main()
{
	thread p0(fun0);
	thread p1(fun1);
	p0.join();
	p1.join();
    return 0;
}

在这种情况下,两个线程一旦一个加了锁,那么另一个就必定阻塞,这样,就不会出现两边加锁两边阻塞的情况,从而避免死锁。

②同时上锁

同时上锁需要用到lock()函数,如下所述:


mutex m0,m1;
int i = 0;
void fun0()
{
	while (i < 100)
	{
                lock(m0,m1);
		lock_guard<mutex> g0(m0, adopt_lock);
		lock_guard<mutex> g1(m1, adopt_lock);
		cout << "thread 0 running..." << endl;
	}
	return;
}
void fun1()
{
	while (i < 100)
	{
                lock(m0,m1);
		lock_guard<mutex> g0(m0, adopt_lock);
		lock_guard<mutex> g1(m1, adopt_lock);
		cout << "thread 1 running...   "<< i << endl;
	}
	return;
}
int main()
{
	thread p0(fun0);
	thread p1(fun1);
	p0.join();
	p1.join();
    return 0;
}

注意到这里的lock_guard中多了第二个参数adopt_lock,这个参数表示在调用lock_guard时,已经加锁了,防止lock_guard在对象生成时构造函数再次lock()。 

以上就是C++多线程之互斥锁与死锁的详细内容,更多关于C++ 多线程 互斥锁 死锁的资料请关注编程网其它相关文章!

--结束END--

本文标题: C++多线程之互斥锁与死锁

本文链接: https://www.lsjlt.com/news/159612.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • C++多线程之互斥锁与死锁
    目录1.前言2.互斥锁2.1 互斥锁的特点2.2 互斥锁的使用2.3 std::lock_guard3.死锁3.1 死锁的含义3.2 死锁的例子3.3 死锁的解决方法1.前言 比如说...
    99+
    2022-11-12
  • python多线程互斥锁与死锁
    目录一、多线程间的资源竞争二、互斥锁1.互斥锁示例2.可重入锁与不可重入锁三、死锁一、多线程间的资源竞争 以下列task1(),task2()两个函数为例,分别将对全局变量num加一...
    99+
    2022-11-13
  • python多线程互斥锁与死锁问题详解
    目录一、多线程共享全局变量二、给线程加一把锁锁三、死锁问题总结一、多线程共享全局变量 代码实现的功能: 创建work01与worker02函数,对全局变量进行加一操作创建main函数...
    99+
    2022-11-13
  • python多线程中互斥锁与死锁的示例分析
    小编给大家分享一下python多线程中互斥锁与死锁的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!一、多线程间的资源竞争以下列task1(),task2()两个函数为例,分别将对全局变量num加一重复一千万次循环(...
    99+
    2023-06-29
  • C#多线程中的互斥锁Mutex
    一、简介 Mutex的突出特点是可以跨应用程序域边界对资源进行独占访问,即可以用于同步不同进程中的线程,这种功能当然这是以牺牲更多的系统资源为代价的。 主要常用的两个方法: publ...
    99+
    2022-11-13
  • Go语言线程安全之互斥锁与读写锁
    目录一、互斥锁是什么?1.概念2.未加锁3.加锁之后二、读写锁【效率革命】1.为什么读写锁效率高2.使用方法三、sync.once1.sync.once产生背景2.sync.once...
    99+
    2022-11-13
  • C#多线程中的互斥锁Mutex怎么用
    本篇内容主要讲解“C#多线程中的互斥锁Mutex怎么用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C#多线程中的互斥锁Mutex怎么用”吧!一、简介Mutex的突出特点是可以跨应用程序域边界对...
    99+
    2023-06-30
  • Linux多线程中fork与互斥锁过程示例
    目录问题提出:(一)初次尝试(二)理性分析(三)解决问题(1)使用pthread_join()(2)使用phread_atfork()注册一个fork之前的判断问题提出: 我们有这样一个问题:在一个多线程程序中创建子进程...
    99+
    2022-06-04
    Linux多线程fork linux互斥锁过程
  • 多线程之死锁详解
    死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果无外力干涉,这些线程将无法继续执行下去。死锁的产生通常...
    99+
    2023-09-13
    多线程
  • C#多线程之线程锁
    目录一、Mutex类二、Mutex的用途三、Semaphore信号量1、简介2、初始化3、WaitOne()和Release()四、Monitor类典型的生产者与消费者实例五、Loc...
    99+
    2022-11-13
  • Java多线程之死锁详解
    目录1、死锁2、死锁经典问题——哲学家就餐问题 总结1、死锁 出现场景:当线程A拥有了A对象的锁,想要去获取B对象的锁;线程B拥有了B对象的锁,想要拥有A对象的锁,两个线程...
    99+
    2022-11-12
  • C++多线程互斥锁和条件变量的详解
    目录互斥锁:std::mutex::try_lock         条件变量:condition_variable总结我们了解互斥...
    99+
    2022-11-13
  • C#多线程死锁介绍与案例代码
    一、死锁简介 在多道程序设计环境下,多个进程可能竞争一定数量的资源,。一个进程申请资源,如果资源不可用,那么进程进入等待状态。如果所申请的资源被其他等待进程占有,那么该等待的进程有可...
    99+
    2022-11-13
  • java线程之死锁
    目录一、什么是死锁二、死锁产生的原因三、死锁演示1、synchronized2、lock四、如何查看死锁1、使用jps命令找到运行程序的pid2、jstack查看栈信息一、什么是死锁...
    99+
    2022-11-13
  • C++详细讲解互斥量与lock_guard类模板及死锁
    目录互斥量的基本概念互斥量的使用lock_guard类模板死锁lock与lock_guard的使用保护共享数据,操作时,用代码把共享数据锁住、操作数据、解锁 其他想操作共享数据的线程...
    99+
    2022-11-13
  • Linux线程互斥锁的概念
    本篇内容介绍了“Linux线程互斥锁的概念”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!在编程中,引入了对象互斥锁的概念,来保证共享数据操作...
    99+
    2023-06-15
  • C语言多线程开发中死锁与读写锁问题详解
    目录死锁读写锁死锁 有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁; 两个或两个以上的进程在...
    99+
    2022-11-13
  • Java多线程之悲观锁与乐观锁
    目录1. 悲观锁存在的问题2. 通过CAS实现乐观锁3. 不可重入的自旋锁4. 可重入的自旋锁总结问题: 1、乐观锁和悲观锁的理解及如何实现,有哪些实现方式? 2、什么是乐观锁和悲观...
    99+
    2022-11-13
  • Python互斥锁怎么解决多线程问题
    这篇文章给大家分享的是有关Python互斥锁怎么解决多线程问题的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。python主要应用领域有哪些1、云计算,典型应用OpenStack。2、WEB前端开发,众多大型网站均...
    99+
    2023-06-14
  • Python实现的多线程同步与互斥锁功能示例
    本文实例讲述了Python实现的多线程同步与互斥锁功能。分享给大家供大家参考,具体如下: #! /usr/bin/env python #coding=utf-8 import threading im...
    99+
    2022-06-04
    示例 多线程 功能
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作