1、互斥量的概念
-
互斥量是一个类对象,理解成一把锁,多个线程尝试用lock()成员函数来加锁这把锁头(成功的标志是lock()函数的返回,不成功将一直阻塞在这把锁上)。
-
只有一个线程能锁定成功(成功的加锁),
-
如果没有获取锁成功,那么该线程将会卡在lock()这个锁头上,不断的尝试去获取。
-
互斥量需要小心使用,数据(临界区)不能过多或者过少的保护,过少有安全问题,过多有效率问题。
-
-
互斥量的用法
- 先获取lock(),操作共享数据,unlock()释放
- lock()和unlock()需要承兑使用,有lock()必然要有unlock
- 每调用一次lock(),必须调用一次unlock(),否则会出现死锁
- 不应该也不允许调用一次lock()却调用一次以上unlock(),即不允许非对称数量的操作
cpp
#include <iostream>
#include <mutex>
#include <thread>
#include <queue>
class MessageQueue{
public:
void inMsgRecvQueue(){
for(int i = 0;i < n;i++){
std::cout << "inMsgRecvQueue()执行, 插入一个元素i = " << i << std::endl;
mutex_lock.lock();
q.push(i);
mutex_lock.unlock();
}
}
void outMsgRecvQueue(){
for(int i = 0;i < n;i++){
int msg = poll_msg();
if(msg > 0){
std::cout << "outMsgRecvQueue()执行, msg = " << msg << std::endl;
}
else{
std::cout << "outMsgRecvQueue()执行, 但此时消息队列中为空" << std::endl;
}
}
}
int poll_msg(){
int msg = -1;
mutex_lock.lock();
if(q.size()){
msg = q.front();
q.pop();
std::cout << "outMsgRecvQueue()执行, msg = " << msg << std::endl;
}
mutex_lock.unlock();
return msg;
}
private:
std::queue<int> q;
static const int n = 100000;
std::mutex mutex_lock;
};
void test1()
{
MessageQueue messageQueue;
std::thread in_thread(&MessageQueue::inMsgRecvQueue, std::ref(messageQueue));
std::thread out_thread(&MessageQueue::outMsgRecvQueue, std::ref(messageQueue));
in_thread.join();
out_thread.join();
}
针对所需要控制串行执行的临界区上锁,执行完毕之后再解锁即可保证。
2、std::lock_guard类模板
- 为了防止使用lock()和unlock()进行非对称数量的操作,C++提供了mutex的类模板
- std::lock_guard直接取代lock()和unlock(),直接使用lock_guard()代替二者
cpp
template<typename _Mutex>
class lock_guard
{
public:
typedef _Mutex mutex_type;
explicit lock_guard(mutex_type& __m) : _M_device(__m){
_M_device.lock();
}
lock_guard(mutex_type& __m, adopt_lock_t) noexcept : _M_device(__m){
} // calling thread owns mutex
~lock_guard(){
_M_device.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
mutex_type& _M_device;
};
这个是std::lock_guard的源码,可以看到lock_guard的源码很简单,在构造时加锁,析构时释放锁
- 构造时加锁没有问题
- 析构时候释放锁似乎不太合理
- 一个类的析构是在其作用域失效时才会析构
- 假设当前函数的代码非常长,那么意味着从构造到函数体全部执行完毕这一串都是临界区
- 这样就会影响整个程序的运行效率
2.1、lock_guard灵活使用
由于防止lock_guard的生命周期过长,通常可以让他提前析构。提前析构的方式就是通过增加{}代码块,把需要临界区的代码放入{}中
cpp
#include <iostream>
#include <mutex>
#include <thread>
#include <queue>
class Lock_Gurad{
public:
void inMsgRecvQueue(){
for(int i = 0;i < n;i++){
std::cout << "inMsgRecvQueue()执行, 插入一个元素i = " << i << std::endl;
{
// 代码块,执行完lock_gurad析构
std::lock_guard<std::mutex> mutex_lock_guard(mutex_lock);
q.push(i);
}
}
}
void outMsgRecvQueue(){
for(int i = 0;i < n;i++){
int msg = poll_msg();
if(msg > 0){
std::cout << "outMsgRecvQueue()执行, msg = " << msg << std::endl;
}
else{
std::cout << "outMsgRecvQueue()执行, 但此时消息队列中为空" << std::endl;
}
}
}
int poll_msg(){
int msg = -1;
// 这里需要等poll_msg()函数执行完才会释放lock_guard
std::lock_guard<std::mutex> mutex_lock_guard(mutex_lock);
if(q.size()){
msg = q.front();
q.pop();
std::cout << "outMsgRecvQueue()执行, msg = " << msg << std::endl;
}
return msg;
}
private:
std::queue<int> q;
static const int n = 100000;
std::mutex mutex_lock;
};
void test_lock_guard()
{
Lock_Gurad lockGurad;
std::thread in_thread(&Lock_Gurad::inMsgRecvQueue, std::ref(lockGurad));
std::thread out_thread(&Lock_Gurad::outMsgRecvQueue, std::ref(lockGurad));
in_thread.join();
out_thread.join();
}
3、死锁与预防
-
产生死锁的必要条件:
-
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
-
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
-
不可剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
-
循环等待条件:在发生死锁时,必然存在一个进程--资源的环形链。
-
-
死锁的预防:
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
- 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
3.1、死锁的演示
- 线程1先拿锁1,再拿锁2
- 线程2先拿锁2,再拿锁1
- 相互持有一把锁,并且期望得到对方手里的锁,导致最后的死锁
cpp
#include <iostream>
#include <mutex>
#include <thread>
#include <queue>
class DeadLock{
public:
void inMsgRecvQueue(){
for(int i = 0;i < n;i++){
std::cout << "inMsgRecvQueue()执行, 插入一个元素i = " << i << std::endl;
mutex_lock1.lock();
mutex_lock2.lock();
q.push(i);
mutex_lock2.unlock();
mutex_lock1.unlock();
}
}
void outMsgRecvQueue(){
for(int i = 0;i < n;i++){
int msg = poll_msg();
if(msg > 0){
std::cout << "outMsgRecvQueue()执行, msg = " << msg << std::endl;
}
else{
std::cout << "outMsgRecvQueue()执行, 但此时消息队列中为空" << std::endl;
}
}
}
int poll_msg(){
int msg = -1;
mutex_lock2.lock();
mutex_lock1.lock();
if(q.size()){
msg = q.front();
q.pop();
}
mutex_lock1.unlock();
mutex_lock2.unlock();
return msg;
}
private:
std::queue<int> q;
static const int n = 100000;
std::mutex mutex_lock1;
std::mutex mutex_lock2;
};
void test_deadlock()
{
DeadLock deadLock;
std::thread in_thread(&DeadLock::inMsgRecvQueue, std::ref(deadLock));
std::thread out_thread(&DeadLock::outMsgRecvQueue, std::ref(deadLock));
in_thread.join();
out_thread.join();
}
3.2、死锁的解决方法
- 多个线程按照相同的顺序持有锁,例如都按照1,2或者2,1的顺序获取就没问题
- std::lock模板函数,同时获取多把锁,下面是std::lock的源码
-
其核心代码很简单也容易理解:当需要多把锁的时候,要么我同时拿到要么我一个也不要。
-
即:如果能获取就获取,获取不到就释放全部的锁
-
最后释放还是需要程序员手动释放两把锁
template<typename _L1, typename _L2, typename... _L3>
void lock(_L1& __l1, _L2& __l2, _L3&... __l3)
{
while (true){
using __try_locker = __try_lock_impl<0, sizeof...(_L3) != 0>;
unique_lock<_L1> __first(__l1);
int __idx;
auto __locks = std::tie(__l2, __l3...);
__try_locker::__do_try_lock(__locks, __idx);
if (__idx == -1){
__first.release();
return;
}
}
}
-
因此可以将死锁代码改成下面这个样子
cpp
void inMsgRecvQueue(){
for(int i = 0;i < n;i++){
std::cout << "inMsgRecvQueue()执行, 插入一个元素i = " << i << std::endl;
std::lock(mutex_lock1, mutex_lock2);
q.push(i);
mutex_lock1.unlock();
mutex_lock2.unlock();
}
}
int poll_msg(){
int msg = -1;
std::lock(mutex_lock1, mutex_lock2);
if(q.size()){
msg = q.front();
q.pop();
}
mutex_lock1.unlock();
mutex_lock2.unlock();
return msg;
}
3.3、std::adopt_lock类模板
而std::lock批量获取锁后为了忘记释放,可以在std::lock_guard中添加参数std::adopt_lock使得在在析构时自动释放锁
- std::lock批量获取锁后, std::lock_guard()函数中加入std::adopt_lock将不会再获取锁,而是等待析构时释放
cpp
void inMsgRecvQueue(){
for(int i = 0;i < n;i++){
std::cout << "inMsgRecvQueue()执行, 插入一个元素i = " << i << std::endl;
std::lock(mutex_lock1, mutex_lock2);
std::lock_guard<std::mutex>(mutex_lock1, std::adopt_lock);
std::lock_guard<std::mutex>(mutex_lock2, std::adopt_lock);
q.push(i);
}
}
int poll_msg(){
int msg = -1;
std::lock(mutex_lock1, mutex_lock2);
std::lock_guard<std::mutex>(mutex_lock1, std::adopt_lock);
std::lock_guard<std::mutex>(mutex_lock2, std::adopt_lock);
if(q.size()) {
msg = q.front();
q.pop();
}
return msg;
}