前言
互斥量的存在就是为了保护多线程中数据的安全。
一、互斥量的用法
1.lock( )、unlack( )
#include<iostream>
#include<list>
#include<thread>
#include<mutex>
using namespace std;
class A
{
public:
void m_read()
{
for (int i = 1; i < 100000; i++)
{
m_mutex.lock();
li.push_back(i);
printf("容器插入数据:%d\n", i);
m_mutex.unlock();
}
}
void m_write()
{
for (int i = 1; i < 100000; i++)
{
m_mutex.lock();
if (!li.empty())
{
int s = li.front();
li.pop_front();
m_mutex.unlock();
}
else
{
m_mutex.unlock();
printf("******容器为空******\n");
}
}
}
private:
mutex m_mutex;
list<int> li;
};
int main()
{
A a;
thread myth1(&A::m_read,&a);
thread myth2(&A::m_write,&a);
myth1.join();
myth2.join();
return 0;
}
当线程1 lock()拿到锁(互斥量)后,线程2将在该步阻塞,并无法对数据A进行操作;当线程1 unlock()释放该锁后,线程2将拿到锁,对数据A进行操作,此时线程1将阻塞,并无法对数据A进行操作。即:一把锁仅能由一个线程拥有,当一个线程拿到该锁后其他线程因无法拿到该锁而阻塞等待,等拿到锁的线程释放该锁,其他线程将尝试拿锁,拿锁成功后,其他线程阻塞等待。
若上面程序不加锁的话,有一定的几率会报错,因为出入操作并不是一步执行完成的,而是在机器内部有多步执行完成,所以在执行插入多步的过程中若切换至另一个线程,则容器不为空,但无完整数据,删除失败,报错。
注意:lock( )、unlack( )要成对出现,有加锁操作必须要解锁操作。尤其是在判断语句前有加锁操作时,在不同条件运行路径内均需要有解锁操作。
2.lock_guard类模板
lock_guard类模板可直接取代lock( )、unlack( ),并且使用lock_guard类模板后不能再对这个保护区域使用lock( )、unlack( )。
void m_read()
{
for (int i = 1; i < 100000; i++)
{
lock_guard<mutex> m_guard(m_mutex);
li.push_back(i);
printf("容器插入数据:%d\n", i);
}
}
lock_guard类模板超出作用域时会自动析构释放锁。
3.unique_lock
unique_lock可以取代lock_guard,实现加锁、自动释放锁的作用。
unique_lock不同于lock_guard的独特性,在于第二个参数的设置
- adopt_lock
作用:标记,通知unique_lock不需要在构造函数中lock这个互斥量了。前提,必须要把互斥量提前lock了,否则报异常。
m_mutex.lock();
unique_lock<mutex> m_unique(m_mutex,adopt_lock);
、、、
//需要保护的数据
、、、
- try_to_lock
作用:尝试拿锁。如果拿到锁了就执行保护数据部分,若没有拿到锁就执行其他部分。这样不会使线程即使没有拿到锁也不会阻塞等待
unique_lock<mutex> m_unique(m_mutex,try_to_lock);
if (m_unique.owns_lock())
{
printf("拿到锁,容器插入数据\n", );
}
else
{
printf("没有拿到锁,暂时不能插入数据\n" );
}
- defer_lock
作用:初始化一个没有加锁的互斥量
unique_lock的成员函数
- lock(),加锁
- unlock(),解锁;
- try_lock(),尝试给互斥量加锁,如果拿不到锁,则返回false,如果拿到了锁,返回true,这个函数不阻塞的;
- release(),返回它所管理的lutext对象指针,并释放所有权﹔也就是说,这个unique_lock和mutext不再有关系。
二、死锁
例如:有两个线程,每个线程有两把锁A和B,线程1拿了A锁,欲拿B锁,同时线程2拿了B锁,欲拿A锁。此时就形成了死锁。只要线程不释放锁就会一直阻塞等待。
class A
{
public:
void m_read()
{
for (int i = 1; i < 100000; i++)
{
m_mutex.lock();
y_mutex.lock();
li.push_back(i);
printf("容器插入数据:%d\n", i);
y_mutex.unlock();
m_mutex.unlock();
}
}
void m_write()
{
for (int i = 1; i < 100000; i++)
{
y_mutex.lock();
m_mutex.lock();
if (!li.empty())
{
int s = li.front();
li.pop_front();
m_mutex.unlock();
y_mutex.unlock();
}
else
{
m_mutex.unlock();
y_mutex.unlock();
printf("******容器为空******\n");
}
}
}
private:
mutex m_mutex;
mutex y_mutex;
list<int> li;
};
此设计就会形成死锁。
如何解决?
使线程对锁的上锁顺序一致,就不会出现死锁。即线程1上锁顺序A、B;线程2上锁顺序A、B.
三、lock函数模板
一次锁住两个或者两个以上的互斥量(至少两个,多了不限,1个不行);
它不存在这种因为再多个线程中因为锁的顺序问题导致死锁的风险问题;
std::lock():如果互斥量中有一个没锁住,它就在那里等着,等所有互斥量都锁住,它才能往下走(返回)﹔要么两个互斥量都锁住,要么两个互斥量都没锁住。如果只锁了一个,另外一个没锁成功,则它立即把已经锁住的解锁。
class A
{
public:
void m_read()
{
for (int i = 1; i < 100000; i++)
{
lock(m_mutex, y_mutex);
li.push_back(i);
printf("容器插入数据:%d\n", i);
y_mutex.unlock();
m_mutex.unlock();
}
}
void m_write()
{
for (int i = 1; i < 100000; i++)
{
lock(m_mutex, y_mutex);
if (!li.empty())
{
int s = li.front();
li.pop_front();
m_mutex.unlock();
y_mutex.unlock();
}
else
{
m_mutex.unlock();
y_mutex.unlock();
printf("******容器为空******\n");
}
}
}
private:
mutex m_mutex;
mutex y_mutex;
list<int> li;
};
那可以不用手动unlock()? 可以,这需要与lock_guard类模板配合使用。
void m_read()
{
for (int i = 1; i < 100000; i++)
{
lock(m_mutex, y_mutex);
lock_guard<mutex> m_guard1(m_mutex, adopt_lock);
lock_guard<mutex> m_guard2(y_mutex, adopt_lock);
li.push_back(i);
printf("容器插入数据:%d\n", i);
}
}
adopt_lock的作用就是在lock_guard m_guard1(m_mutex, adopt_lock);中对m_mutex不上锁,因为上面语句已经对其上锁了,不可再加锁。
四、总结
虽然有多种锁,但是最好还是一个一个的锁,使用lock( )、unlack( )。