互斥量概念、用法、死锁演示及解决详解

发布时间 2023-08-17 14:30:15作者: NoAcalculia

互斥量概念、用法、死锁演示及解决详解

视频:https://www.bilibili.com/video/BV1Yb411L7ak?p=7&vd_source=4c026d3f6b5fac18846e94bc649fd7d0
参考文章:https://blog.csdn.net/qq_38231713/article/details/106091902

互斥量(mutex)

如果想深入了解可以具体看一下操作系统互斥量的讲解

  • 互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。
  • 互斥量使用要小心,保护数据不多也不少,少了达不到效果,多了影响效率。

互斥量的用法

lock()、unlock()

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

class A {
public:
	void inMsgqueue() {
		for (int i = 0; i < 10000; i++) {
			cout << "inMsgqueue()执行,插入元素" << i << endl;
			my_mutex.lock();
			msgqueue.push_back(i);
			my_mutex.unlock();
		}
	}
	void outMsgqueue() {
		for (int i = 0; i < 10000; i++) {
			if (!msgqueue.empty()) {
				int command = msgqueue.front();
				my_mutex.lock();
				msgqueue.pop_front();
				my_mutex.unlock();
			}
			else {
				cout << "outMsgqueue()执行,但为空" << i << endl;
			}
		}
		cout << "end" << endl;
	}
private:
	std::list<int>msgqueue;
	std::mutex my_mutex;
};

int main() {
	
	A myobject;
	thread myoutmsgobj(&A::outMsgqueue, &myobject);
	thread myinmsgobj(&A::inMsgqueue, &myobject);
	myinmsgobj.join();
	myoutmsgobj.join();
	cout << "主线程开始" << endl;
	return 0;
}

这样一个时间段内就只能一个数据存或者读

lock_guard类用法

  • lock_guard sbguard(myMutex);取代lock()和unlock()
  • lock_guard构造函数执行了mutex::lock();在作用域结束时,调用析构函数,执行mutex::unlock()
    img

死锁

死锁的条件

  • 互斥条件(Mutual Exclusion):至少有一个资源只能被一个进程(线程)同时占用,即该资源一次只能服务一个请求。这意味着当一个进程(线程)占用了该资源后,其他进程(线程)必须等待该资源释放。

  • 持有并等待条件(Hold and Wait):一个进程(线程)在等待其他进程(线程)所持有的资源时,持有自己已占用的资源不释放。换句话说,进程(线程)在等待资源的同时,继续持有已占用的资源。

  • 不可抢占条件(No Preemption):已分配给进程(线程)的资源不能被强制性地抢占,只能在进程(线程)使用完后自愿释放。

  • 循环等待条件(Circular Wait):在系统中存在一个进程(线程)资源的循环链,即每个进程(线程)都在等待下一个进程(线程)所持有的资源。

死锁例子

  • 进程A获取资源X。
  • 进程B获取资源Y。
  • 进程A尝试获取资源Y,但由于被进程B占用,所以A被阻塞。
  • 进程B尝试获取资源X,但由于被进程A占用,所以B被阻塞。
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mutex1;
std::mutex mutex2;

void func1() {
    std::lock_guard<std::mutex> lock1(mutex1);
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 为了让两个线程有足够的时间相互等待
    std::lock_guard<std::mutex> lock2(mutex2);
    std::cout << "Thread 1 executed successfully" << std::endl;
}

void func2() {
    std::lock_guard<std::mutex> lock2(mutex2);
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 为了让两个线程有足够的时间相互等待
    std::lock_guard<std::mutex> lock1(mutex1);
    std::cout << "Thread 2 executed successfully" << std::endl;
}

int main() {
    std::thread t1(func1);
    std::thread t2(func2);
    t1.join();
    t2.join();
    
    std::cout << "Main thread executed successfully" << std::endl;
    return 0;
}

死锁的解决方案

死锁四个条件破坏一个即可

视频老师只讲了只要保证多个互斥量上锁的顺序一样就不会造成死锁。但其实正规来说是破坏一个条件即可

std::lock()函数模板

当多个线程需要同时获取多个互斥量的锁时,如果不谨慎处理,可能会产生死锁的情况。而std::lock()函数模板的作用就是帮助我们避免死锁。

通俗地说,std::lock()函数模板可以同时获取多个互斥量的锁,或者不获取任何锁。它的作用就像是一个协调者,帮助多个线程按照某种顺序获取锁,以避免它们之间相互等待的死锁情况。

假设有两个线程A和B,它们分别需要获取互斥量X和Y的锁。如果A先获取了锁X,而B先获取了锁Y,那么它们就会相互等待对方释放锁,导致死锁。但是,如果我们使用std::lock()函数模板来同时获取锁X和锁Y,它会按照某种顺序获取锁,确保两个线程都能成功获取所需的锁。如果无法同时获取所有锁,std::lock()函数会自动释放已经获取的锁,以避免死锁的发生。
img
上述图片展示了std::lock()的用法,可以替代某些语句

std::lock_guard的std::adopt_lock参数

在使用std::lock_guard时,可以通过将std::adopt_lock作为额外的参数传递给它来指示它应该"接管"已经获得的互斥量的所有权。

通俗地说,当我们使用std::lock_guard时,它会在构造函数中自动获取互斥量的锁,并在析构函数中自动释放锁。默认情况下,std::lock_guard会尝试在构造函数中获取锁。然而,有时候我们已经手动获取了互斥量的锁,此时我们不希望std::lock_guard再次获取锁,而是直接接管已经获得的锁的所有权。这时候就可以使用std::adopt_lock参数。

简单来说,std::adopt_lock的作用是告诉std::lock_guard,互斥量的锁已经被手动获取,不需要再重新获取。std::lock_guard接收到这个参数后,会假定互斥量的锁已经被获取,并在析构函数中自动释放锁。

这样做的好处是,我们可以在代码的不同部分使用不同的方式获取锁,而不担心重复获取锁导致死锁或其他问题。通过使用std::adopt_lock参数,我们可以告诉std::lock_guard使用已经手动获取的锁,确保代码的正确性和安全性。
img
就好比这份代码,已经用lock给两个互斥量上锁了,不能再用lock_guard给他上锁了,只能用它的解锁,std::adopt_lock是个结构体对象,起一个标记作用;作用就是表示这个互斥量已经lock();