c++多线程之互斥锁mutex
互斥锁mutex简介
mutex只有两种状态,即上锁( lock )和解锁( unlock ),它具有原子性和唯一性,简单解释就是:
- 原子性:一个互斥锁同一时间只有一个线程可以对其进行上锁。
- 唯一性:当一把互斥锁被上锁后,在它被解锁前不能被其他线程锁定。
下面我们具体看一下mutex类。我们只需掌握它三个接口即可。
void lock() | 上锁,若此时锁被其他线程拥有则阻塞直到锁被释放 |
---|---|
void unlock() | 解锁,此时该线程应已经拥有锁 |
bool try_lock() | 尝试上锁,不会阻塞线程,若成功则返回true,失败返回false |
在具体讲解mutex的使用之前有必要给大家介绍一下临界区。临界区是指线程独占共享资源的代码区,线程在其临界区进行对共享资源的操作。当然对共享资源的操作是互斥的,因此在进入临界区前需要先上锁。所以临界区其实就是跟随在上锁操作后的代码区,看下面伪代码:
线程1{
...
mutex.lock();//上锁
/*******临界区*******/
vector.push_back(value);//将数据插入到容器中
/********************/
mutex.unlock();//解锁
...
}
线程2{
...
mutex.lock();//上锁
/*******临界区*******/
vector.top();//读取容器中数据
/********************/
mutex.unlock();//解锁
...
}
调用lock()
线程1在获得锁后进入临界区代码,对共享容器进行写入操作,此时线程2无法获得锁,如果执行上锁操作将被阻塞,无法进入临界区。直到线程1临界区代码执行完毕解锁,线程1被唤醒进入就绪状态,然后获得锁进入临界区执行。下面我们看个具体例子。
#include<iostream>
#include<thread>
#include<cstdlib>
#include <vector>
#include <mutex>
#include <Windows.h>
using namespace std;
vector<int> vec;
mutex my_mutex;
void fn1(int n)
{
my_mutex.lock();
cout << "----this is a thread for fn1----\n";
for (int i = 0; i < n; i++)
{
vec.push_back(i);
Sleep(100);
}
my_mutex.unlock();
}
void fn2(int n)
{
my_mutex.lock();
cout << "----this is a thread for fn2----\n";
for (int i = 0; i < n; i++)
{
vec.push_back(i);
Sleep(100);
}
my_mutex.unlock();
}
int main()
{
thread t1(fn1,10);
thread t2(fn2,10);
t1.join();
t2.join();
for (vector<int>::iterator iter = vec.begin(); iter != vec.end(); iter++)
cout << *iter << " ";
getchar();
}
输出:
----this is a thread for fn1----
----this is a thread for fn2----
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
调用try_lock()
我们使用try_lock()看一下不同:
void fn1(int n)
{
if (!my_mutex.try_lock())
{
cout << "被其他线程锁定,fn1等待...\n";
while (!my_mutex.try_lock());
}
cout << "----this is a thread for fn1----\n";
for (int i = 0; i < n; i++)
{
vec.push_back(i);
Sleep(100);
}
my_mutex.unlock();
}
void fn2(int n)
{
if (!my_mutex.try_lock())
{
cout << "被其他线程锁定,fn2等待...\n";
while (!my_mutex.try_lock());
}
cout << "----this is a thread for fn2----\n";
for (int i = 0; i < n; i++)
{
vec.push_back(i);
Sleep(100);
}
my_mutex.unlock();
}
int main()
{
thread t1(fn1,10);
thread t2(fn2,10);
t1.join();
t2.join();
for (vector<int>::iterator iter = vec.begin(); iter != vec.end(); iter++)
cout << *iter << " ";
getchar();
}
输出:
----this is a thread for fn1----
被其他线程锁定,fn2等待...
----this is a thread for fn2----
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
我们可以看到try_lock()无论是否获得锁都不会阻塞,而是返回状态,适用于未获得锁的情况下不允许阻塞的情形。
死锁
我们假定发生如下情况,在某一时间线程1独占锁A等待独占锁B,在线程1准备独占锁B时线程2独占了锁B并等待独占锁A,此时线程双方都因对方持有了自身需要的锁进入阻塞,但双方都需对方唤醒,所以双方永远不会被唤醒,这就是死锁。死锁是多线程编程中最常见最严重的错误之一,我们要尤其重视。看下面代码:
void thread1()
{
mtx1.lock();
Sleep(1000);
mtx2.lock();
cout << "t1" << "进入临界区\n";
mtx2.unlock();
mtx1.unlock();
}
void thread2()
{
mtx2.lock();
Sleep(1000);
mtx1.lock();
cout << "t2" << "进入临界区\n";
mtx1.unlock();
mtx2.unlock();
}
int main()
{
thread t1(thread1);
thread t2(thread2);
t1.join();
t2.join();
}
运行起来会发现,两个线程都被阻塞无法进入各自的临界区,无论你等待多久两个线程都无法被唤醒,更糟糕的是因为将两个线程都与主线程连接起来,此时主线程也在等待子线程返回,导致主线程也什么都做不了。那改如何避免死锁的发生呢?其实很简单,就以上述代码为例做如下更改:
void thread1()
{
mtx1.lock();
Sleep(1000);
mtx2.lock();
cout << "t1" << "进入临界区\n";
mtx2.unlock();
mtx1.unlock();
}
void thread2()
{
mtx1.lock();
Sleep(1000);
mtx2.lock();
cout << "t2" << "进入临界区\n";
mtx2.unlock();
mtx1.unlock();
}
此时再运行会发现没有问题,由此我们可以发现死锁的发生是因为两个线程获取锁的顺序相反,这样两个线程都有可能在同一时间获取对方锁需要的锁从而导致死锁,我们只需将两线程获取锁的顺序设为一致便可避免死锁的发生。由此引出多线程编程中一个很重要的原则:当两个以上的线程需要获取相同的多个锁时,我们在各线程中以相同的顺序获取它们。但如果我们仔细想想,此时产生死锁的第一诱因是我们使用了多个锁,如果我们坚持只使用一个锁,那么光凭使用锁是不可能发生死锁的。但需要注意的是,除了我们避免在一个函数中亲自获取多个锁外,当我们获取了一个锁后,还要避免调用其他用户提供的代码,因为有可能在其中有包含获取锁的操作,而我们并不得而知。综上我们总结出避免死锁的三个原则:
- 尽量在一个线程只获取一个锁;
- 当获取一个锁后,不调用其他用户提供的代码;
- 如果必须使用多个锁,在每个线程函数中以相同顺序获取它们。
虽然坚持了上述原则并不能完全杜绝死锁的发生,但也避免了大多数发生死锁的情形。所以养成好习惯是十分重要的。
饥饿
相信我们都遇到过这样的事情,我们正准备经过一道门时,对面正好也有一个人准备经过,我们处于礼貌主动让别人先过,谁知对方也是相同的想法,于是相互礼让谁也不愿先过。相似的事情也发生在多线程中,当多个线程都尝试获得同一个资源时,因为条件没有满足或每个线程的优先级没有设置完善,导致多个线程互相等待其他线程先获取资源,从而导致资源在一段时间内闲置,甚至更糟糕的话永远无法被获取(饿死)。饥饿发生的诱因十分多样,需要根据具体问题分析,在这里就不为大家总结,后面将在讲解几种经典的线程间通信的例子中具体为大家分析。