互斥量mutex
- 互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。
- 互斥量使用要注意,保护数据不要多也不要少,少了达不到效果,多了影响效率。
互斥量用法:
#include < mutex >
//包含头文件- 先
lock()
,操作共享数据;unlock()
释放锁; lock()
和unlock()
必须成对使用;
例子代码:
//包含头文件
#include <mutex>
class MyPrint {
public:
//把收到的消息加入消息队列
void inMsgRecvQueue() {
for (int i = 0; i < 10000; i++) {
cout << "插入一个元素到消息队列";
//互斥量mutex的使用,保护共享资源
m_mutex.lock(); //上锁
msgRecvQueue.push_back(i);
m_mutex.unlock(); //释放锁
}
}
//把消息从消息队列取出
void outMsgRecvQueue() {
int command = 0; //指令为command;
for (int i = 0; i < 10000; i++) {
bool result = outMsgLULProc(command); //将所有对共享数据的访问都封装成一个函数,方便加锁
if (result) {
//消息队列不为空
//对根据命令对数据进行处理
}
else {
//消息队列为空
cout << "消息队列为空!" << endl;
}
}
}
bool outMsgLULProc(int &command) {
//对共享资源上锁
m_mutex.lock();
if (!msgRecvQueue.empty()) {
//消息不为空
int command = msgRecvQueue.front(); //返回第一个元素,但不检查是否存在
msgRecvQueue.pop_front();
m_mutex.unlock();//释放锁
return true;
}
m_mutex.unlock(); //所有分支都必须有unlock();
return false;
}
list<int> msgRecvQueue; //消息队列
mutex m_mutex; //创建一个互斥量
};
步骤如下:
- 添加头文件
#include < mutex >;
- 在类的成员中添加
mutex m_mutex;
- 对所有共享资源访问的代码:
lock();
、共享资源访问、unlock();
(注意lock()必须和unlock()配对,涉及多个分支的时候,必须检查所有分支出口都有unlock();
lock_guard()
因为lock()和unlock()必须强制配对,不然会出现各种意想不到的错误,为了避免人为的忘记,C++引入了std::lock_guard
类模板,帮用户调用unlock();
std::lock_guard
可以直接取代lock()和unlock()
:
lock_guard<mutex> mutexGuard(m_mutex);
//m_mutex.lock();
//访问临界资源
//m_mutex.unlock();
本质上,lock_guard是巧用了局部变量的生命周期
- lock_guard类在构造函数中调用了
lock();
- lock_guard类在析构函数中调用了
unlock();
lock_guard不够灵活,必须要析构的时候才能unlock(),我们不能按照自己想要的逻辑进行unlock(),想要提前unlock(),可以巧用生命周期:
{
lock_guard<mutex> mutexGuard(m_mutex);
//m_mutex.lock();
//访问临界资源
//m_mutex.unlock();
}
加上{ }
在限制mutexGuard的生命周期,在我们想要它unlock()的时候就结束他的生命周期。
死锁
互斥资源、请求与保持、不可剥夺、环路等待。
一个线程,占有资源,又想去抢占其它资源,就可能会产生死锁。
死锁的产生,必须要求至少两个互斥量(mutex1和mutex2)
- 线程A执行时,这个线程先
mutex1.lock()
,并且成功上锁,然后去访问mutex2的时候,出现了进程切换。 - 线程B执行的时候,这个线程先
mutex2.lock()
,并且也成功上锁,然后想去访问mutex1,这时候需要对mutex1上锁,但是mutex1此时被线程A lock()了,所以必须等线程A 调用mutex1.unlock();
才能继续执行,而线程A 想要继续执行就必须要访问mutex2,此时mutex2被B lock()住,A又必须等B 调用mutex2.unlock();
这样,A,B就陷入了死锁。
thread A{
//访问资源1,调用mutex1锁
mutex1.lock();
//访问资源1.....
//访问资源2,调用mutex2锁
mutex2.lock();
//访问资源2
mutex2.unlock();
mutex1.unlock();
}
thread B{
//访问资源2,调用mutex2锁
mutex2.lock();
//访问资源2.....
//访问资源1,调用mutex1锁
mutex1.lock();
//访问资源1
mutex1.unlock();
mutex2.unlock();
}
死锁的产生原因:互斥量mutex的嵌套使用,而且多个线程之间嵌套的顺序不同。
死锁的一般解决方案:只要保证多个互斥量上锁的顺序一样,就可以避免死锁。
lock()
std::lock
函数模板,可以同时锁上两个及以上的互斥量。
它可以减少在多个线程中,因为互斥量上锁的顺序不同引起的死锁问题。
std::lock(mutex1,mutex2……)
:如果互斥量中一个没锁住,它就等着,等所有互斥量都锁住,才能继续执行。如果有一个没锁住,就会把已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁)
std::lock(mutex1, mutex2);
//上面等价于调用
mutex1.lock();
mutex2.lock();
//因此后面必须调用
mutex2.unlock();
mutex2.unlock();
因此unlock()必须由程序员管控,为了避免忘记,可以结合lock_guard()调用:
std::lock_guard的std::adopt_lock参数
std::lock_guardstd::mutex my_guard(my_mutex,std::adopt_lock);
加入adopt_lock后,在调用lock_guard的构造函数时,不再进行lock();- adopt_guard为结构体对象,起一个标记作用,表示这个互斥量已经lock(),不需要在lock()。
std::lock(mutex1, mutex2);
std::lock_guard<mutex> mutexGuard1(mutex1, std::adopt_lock);
std::lock_guard<mutex> mutexGuard2(mutex2, std::adopt_lock);
//上述等价于调用了
mutex1.lock();
mutex2.lock();
//....
mutex2.unlock();
mutex2.unlock();