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

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

5.1 互斥量(mutex)的基本概念

保护共享数据,操作时,用代码把共享数据锁住,操作数据,解锁,数据被锁时,其他像操作共享数据必须等待解锁。

5.2 互斥量的用法

互斥量是个类对象。理解成一把锁,多个线程尝试用lock()成员函数来加锁头,只有一个线程能够锁成功(成功的标志是lock()函数返回),如果没锁成功,那么流程卡在lock()这里不断的尝试去锁这把锁头。

互斥量使用要小心,保护数据不多也不少,少了,没达到保护效果,多了,影响效率。

5.2.1 lock(), unlock()

lock()unlock()要成对使用,有lock()必然有unlock(),每调用一次lock(),必然应该调用一次unlock()

代码5-2-1

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

class A {
public:
    // 把收到的消息(玩家命令)入到一个队列的线程
    void inMsgRecvQueue() {
        for (int i = 0; i < 1000; ++i) {
            cout << "inMsgRecvQueue执行,插入一个元素" << i << endl;
            m_mutex.lock();
            msgRecvQueue.push_back(i); // 假设i就是收到玩家的命令,直接弄到消息队列里面
            m_mutex.unlock();
        }
    }
    // 把数据从消息队列中取出的线程;
    void outMsgRecvQueue() {
        for (int i = 0; i < 1000; ++i) {
            m_mutex.lock();
            if (!this->msgRecvQueue.empty()) {
                int command = this->msgRecvQueue.front();
                this->msgRecvQueue.pop_front();
                cout << "outMsgRecvQueue执行,命令为:" << command << endl;
            } else {
                cout << "outMsgRecvQueue执行,消息队列为空" << endl;
            }
            m_mutex.unlock();
        }
    }
private:
    list<int> msgRecvQueue; // 容器,专门用于代表玩家给咱们发送过来的命令。
    mutex m_mutex;
};

int main() {
    A a;
    thread out_t(&A::outMsgRecvQueue, &a); // 第二个参数是 引用, 才能保证线程里,用的是同一个对象
    thread in_t(&A::inMsgRecvQueue, &a);
    out_t.join();
    in_t.join();
    return 0;
}

运行结果

...
inMsgRecvQueue执行,插入一个元素37
inMsgRecvQueue执行,插入一个元素38
inMsgRecvQueue执行,插入一个元素39
inMsgRecvQueue执行,插入一个元素40
inMsgRecvQueue执行,插入一个元素41
outMsgRecvQueue执行,命令为:1
inMsgRecvQueue执行,插入一个元素42
inMsgRecvQueue执行,插入一个元素43
inMsgRecvQueue执行,插入一个元素44
...

5.2.2 std::lock_guard类模版

为了防止编程当中忘记unlock(),引入一个叫std::lock_guard的类模版:你忘记unlock不要紧,我替你unlock

原理:在lock_guard类中,构造函数有lock(),析构函数中有unlock(),对象生命周期结束时,所以会自动调用unlock()

代码5-2-2

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

class A {
public:
    // 把收到的消息(玩家命令)入到一个队列的线程
    void inMsgRecvQueue() {
        for (int i = 0; i < 1000; ++i) {
            cout << "inMsgRecvQueue执行,插入一个元素" << i << endl;
            m_mutex.lock();
            msgRecvQueue.push_back(i); // 假设i就是收到玩家的命令,直接弄到消息队列里面
            m_mutex.unlock();
        }
    }
    // 把数据从消息队列中取出的线程;
    bool outMsgHelper() {
        lock_guard<mutex> sbguard(m_mutex);
        if (!msgRecvQueue.empty()) {
            int command = msgRecvQueue.front();
            msgRecvQueue.pop_front();
            cout << "outMsgRecvQueue执行,命令为:" << command << endl;
            return true;
        }
        cout << "outMsgRecvQueue执行,消息队列为空" << endl;
        return false;
    }
    void outMsgRecvQueue() {
        for (int i = 0; i < 1000; ++i) {
            outMsgHelper();
        }
    }
private:
    list<int> msgRecvQueue; // 容器,专门用于代表玩家给咱们发送过来的命令。
    mutex m_mutex;
};

int main() {
    A a;
    thread out_t(&A::outMsgRecvQueue, &a); // 第二个参数是 引用, 才能保证线程里,用的是同一个对象
    thread in_t(&A::inMsgRecvQueue, &a);
    out_t.join();
    in_t.join();
    return 0;
}

运行结果

...
outMsgRecvQueue执行,消息队列为空
outMsgRecvQueue执行,消息队列为空
outMsgRecvQueue执行,消息队列为空
outMsgRecvQueue执行,消息队列为空inMsgRecvQueue执行,插入一个元素
outMsgRecvQueue执行,消息队列为空
outMsgRecvQueue执行,消息队列为空
outMsgRecvQueue执行,消息队列为空
...

为了增加lock_guard的灵活性,可以用花括号来限制lock_guard对象的生命周期

代码5-2-3

bool outMsgHelper() {
		{
		...处理其他逻辑
			lock_guard<mutex> sbguard(m_mutex);
      if (!msgRecvQueue.empty()) {
          int command = msgRecvQueue.front();
          msgRecvQueue.pop_front();
          cout << "outMsgRecvQueue执行,命令为:" << command << endl;
          return true;
      }
      cout << "outMsgRecvQueue执行,消息队列为空" << endl;
      return false;
		}
		...处理其他逻辑
}

运行结果

5.3 死锁

比如我有两把锁(死锁这个问题,是由至少两个所有也就是两个互斥量才能产生),金锁(GoldLock),银锁(SilverLock),两个线程A,B,进行以下过程:

  • 线程A执行的时候,这个线程先锁GoldLock,把GoldLock锁成功后,然后去锁SilverLock
  • 出现了上下文切换。
  • 线程B执行了,这个线程先锁SilverLock,因为银锁还没有被锁,所以银锁会lock成功,然后去锁GoldLock
  • 此时此刻,因为A锁不了SilverLock,B锁不了GoldLock,A和B的流程都走不下去,A和B产生了死锁。

5.3.1 死锁的演示

代码5-3-1

​ A和B锁的次数要设置得比较多(即代码中的i变量),不然观察不到互锁的现象,并不是不会发生,是发生的概率太小了。

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

class E {
public:
    void A() {
        int i = 1000;
        while(i--) {
            GoldLock.lock();
            SilverLock.lock();
            m_name = "A";
            cout << m_name << endl;
            // 下面的顺序无所谓
            SilverLock.unlock();
            GoldLock.unlock();
        }
    }
    void B() {
        int i = 1000;
        while(i--) {
            SilverLock.lock();
            GoldLock.lock();
            m_name = "B";
            cout << m_name << endl;
            GoldLock.unlock();
            SilverLock.unlock();
        }
    }
private:
    string m_name;
    mutex GoldLock, SilverLock;
};

int main() {
    E e;
    thread t_a(&E::A, &e);
    thread t_b(&E::B, &e);
    t_a.join();
    t_b.join();
    return 0;
}

运行结果

...
A
A
A
A
A
A
A
A

执行到最后一行便停止了,产生了死锁

5.3.2 死锁的一般解决方案

破坏死锁的必要条件之循环等待:将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。在代码5-3-1修改为如下即可:

void A() {
    int i = 1000;
    while(i--) {
        GoldLock.lock();
        SilverLock.lock();
        m_name = "A";
        cout << m_name << endl;
        SilverLock.unlock();
        GoldLock.unlock();
    }
}
void B() {
    int i = 1000;
    while(i--) {
        // 交换了顺序
        GoldLock.lock();
        SilverLock.lock();
        m_name = "B";
        cout << m_name << endl;
        // 下面的顺序无所谓
        GoldLock.unlock();
        SilverLock.unlock();
    }
}

5.3.3 std::lock()函数模版

能力一次锁住多个锁头(至少两个,多了不限),在多线程中,它不会因资源的请求顺序而产生死锁。如果有一个互斥量没锁住,它就释放已经请求到的所有资源,过一段时间,再以相同的方式去请求。

代码5-3-2

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

class E {
public:
    void A() {
        int i = 1000;
        while(i--) {
            lock(GoldLock, SilverLock);
            m_name = "A";
            cout << m_name << endl;
            SilverLock.unlock();
            GoldLock.unlock();
        }
    }
    void B() {
        int i = 1000;
        while(i--) {
            lock(GoldLock, SilverLock);
            m_name = "B";
            cout << m_name << endl;
            GoldLock.unlock();
            SilverLock.unlock();
        }
    }
private:
    string m_name;
    mutex GoldLock, SilverLock;
};

int main() {
    E e;
    thread t_a(&E::A, &e);
    thread t_b(&E::B, &e);
    t_a.join();
    t_b.join();
    return 0;
}

运行结果

...
A
A
A
A
A
A
B
B
...

5.3.4 std::lock_guard的std::adopt_lock参数

因为用std::lock()解决了死锁问题,但是我们还需要关心unlock(),要全部解锁完毕,那怎么操作才能自动解锁呢?之前我们用到了std::lock_guard(),刚好这个可以解决我们的问题~

代码5-3-4

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

class E {
public:
    void A() {
        int i = 10000;
        while(i--) {
            lock(GoldLock, SilverLock);
            lock_guard<mutex> guardGoldLock(GoldLock, adopt_lock);
            lock_guard<mutex> guardSilverLock(SilverLock, adopt_lock);
            m_name = "A";
            cout << m_name << endl;
        }
    }
    void B() {
        int i = 10000;
        while(i--) {
            lock(GoldLock, SilverLock);
            lock_guard<mutex> guardGoldLock(GoldLock, adopt_lock);
            lock_guard<mutex> guardSilverLock(SilverLock, adopt_lock);
            m_name = "B";
            cout << m_name << endl;
        }
    }
private:
    string m_name;
    mutex GoldLock, SilverLock;
};

int main() {
    E e;
    thread t_a(&E::A, &e);
    thread t_b(&E::B, &e);
    t_a.join();
    t_b.join();
    return 0;
}

运行结果

...
A
A
A
A
B
B
B
B
B
B
...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章