死锁的理解
通俗的解释
用自己的话通俗的解释是,多个线程竞争一个锁,从而对共享的数据进行同步操作,在竞争中,由于锁的存在,需要每个线程排队,占有该锁,才能进行后续操作。因此,每个线程为了完成任务,第一步就是需要占据锁。
而死锁发生在这种情况下:
- 线程A首先占据了锁,并执行操作,在这个过程中,线程A没有unlock;
- 此时,线程B也希望占据锁,从而执行任务,但是因为线程A没有unlock,自己只能等待;
- 而线程A如果要unlock,需要满足一定的条件,这个条件需要通过线程B执行完成才能实现。
据此,出现,线程A等待线程B执行完才能unlock,而线程B需要线程A先unlock,才能继续执行,确定死锁,其核心是线程A和线程B相互等待对方先执行,来满足自己。这种当不同线程之间的执行存在依赖关系时,以至于每个线程必须满足一定条件才能执行,但是又不释放对锁的控制权,以至于其他线程无法得到锁的控制权,以便实现之前线程unlock需要的条件,从而发生线程A等待线程B完成以满足条件。
例子1:不用同步量也会发生死锁
死锁的发生不一定要用mutex等互斥量时才会发生,如下,当两个线程之间互相等待就有可能发生。下述代码中每个线程都在等待对象先满足条件,如果不满足条件,对应的线程就继续等下去。至于输出,则依赖系统对这两个线程的调度。
int g_i = 0;
void thread_task1(){
cout << "thread task1" << endl;
while(g_i != 2){
continue;
}
g_i = 1;
}
void thread_task2(){
cout << "thread task2" << endl;
while(g_i != 1){
continue;
}
g_i = 2;
}
int main(){
thread t1(thread_task1);
thread t2(thread_task2);
t1.join();
t1.join();
return 0;
}
例子2:使用一个同步量时
当死锁出现时,明显的症状是发生了阻塞行为。如果多个thread之间没有依赖关系,则只会乱序。只有需要同步时才会出现死锁,而同步的实现可以是任何方式,如上述的循环等待,或者加上了mutex信号量。因此死锁是同步出现问题的表现,不是线程本身的问题。
另一个使用一个mutex同步时引起死锁的简单例子(虽说简单,也设计了一会,其实对于理解死锁还是有帮助的)如下。
vector<int> v = {0, 0};
mutex m;
mutex m1;
mutex m2;
void thread_task1(vector<int> &v){
m.lock();
cout << "lock_guard get m from thread 1" << endl;
v[0] = 1;
if (v[1] == 2) {
cout << "from thread 1: " << v[0] << endl;
m.unlock();
}
}
void thread_task2(vector<int> &v){
m.lock();
cout << "lock_guard get m from thread 2" << endl;
v[1] = 2;
if (v[0] == 1) {
cout << "from thread 2: " << v[1] << endl;
m.unlock();
}
}
int main(){
thread t1(thread_task1, ref(v));
thread t2(thread_task2, ref(v));
t1.join();
t2.join();
}
/** output
lock_guard get m from thread 1
之后一直等待......
*/
多个线程竞争一个锁,注意是一个锁,也就对应一个mutex,这样不同的线程需要排队占有这个锁,才能执行对应的操作。如上代码中,有两个线程t1和t2,分别执行相应的操作,通过mutex m实现同步,在线程t1执行操作时,需要满足v[1] == 2,但是这个写入操作需要线程t2完成,但是此时线程t1占据m,所以线程t2没有机会修改条件,因此发生死锁。分析如下,这里我想到受精卵形成的一个机制——Cortical reaction,用来形容这种情况很是贴切。
此时感受到各种知识的相似之美。
每个mutex对象可以对resource进行lock和unlock,且同一时刻只能允许一个thread占据该mutex,其他的需要排队,多像受精卵排斥多个精子入内的操作。当一个线程执行时,需要满足一个条件,而这个条件需要还未占据mutex的其他线程实现,因此占据mutex的线程就要等待,但是不释放控制权,其他线程无法入内,因此就不能达成条件。
在实验中,我们也想验证如下的场景:
void thread_task1(vector<int> &v){
m1.lock();
cout << "lock_guard get m from thread 1" << endl;
v[0] = 1;
if (v[1] == 2) {
cout << "from thread 1: " << v[0] << endl;
m1.unlock();
}
}
void thread_task2(vector<int> &v){
m2.lock();
cout << "lock_guard get m from thread 2" << endl;
v[1] = 2;
if (v[0] == 1) {
cout << "from thread 2: " << v[1] << endl;
m2.unlock();
}
}
每个线程对应不同的mutex对象,实际上这两个线程并不是同步的关系,而是各自独立运行,完全没有依赖。这种情况下不会导致死锁,因为涉及多个mutex,分析如下:
两个线程分别在不同的sync scope中,也根本就没有进行同步,也就谈不上出现死锁了。
例子3:使用多个同步量时
另外,我这里举的例子是涉及一个mutex,对于涉及多个mutex的场景,也可能出现死锁的问题,如文章所示。
std::mutex m1;
std::mutex m2;
auto f1 = [&m1, &m2]()
{
std::lock_guard<std::mutex> lg1(m1);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> lg2(m2);
};
auto f2 = [&m1, &m2]()
{
std::lock_guard<std::mutex> lg1(m2);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> lg2(m1);
};
std::thread t1([&f1, &f2](){ f1(); });
std::thread t2([&f1, &f2](){ f2(); });
t1.join();
t2.join();
上述代码分析如下:
调试死锁的问题向来就是比较困难的,我在写本文的时候,又去熟悉了一下gdb调试死锁的操作,可以参考这篇文章,还可以对已经运行的程序查看是否死锁,如文章。
为什么会出现
综合以上的分析,我们总结一下deadlock为啥会出现,这里直接借助学术派的总结,如文章所述。
Four conditions that must hold for a deadlock to be possible:
- Mutual exclusion: processes require exclusive control of its resources (not sharing).
- Hold and wait: process may wait for a resource while holding others.
- No preemption: process will not give up a resource until it is finished with it. Also, processes are irreversible: unable to reset to an earlier state where resources not held.
- Circular wait: each process in the chain holds a resource requested by another.
其中还有一个图生动的描述了这些条件的含义:
如果上述条件中有一个被打破,死锁就不会发生。对应我们上述的例子,分别分析这4种条件:
Mutual exclusion
对于互斥量mutex,一次只能被thread占据,如果当前有thread占据该mutex,其他的thread只能等待。
Hold and wait
线程t1需要访问数组中的数据,当满足一定的条件后才会执行,即便当前无法继续前进,也会阻塞在这里,不会释放资源。
No preemption
该线程t1会一直占据该mutex,不会放弃,因此别人无法通过抢占得到,即先占为王,直到满足条件才会unlock。
Circular wait
基于上述的条件,每个线程对象不可能中途退出,因此多个线程之间存在循环等待,因此只能僵持在这里,即线程t1需要等待t2修改条件,但是却释放资源。
对于例2的解决
对于上述条件中,我们只要能打破一个,就能解决死锁问题。对于本文的死锁例子,针对上述的4个条件中,我们可以打破Hold and wait实现死锁的解决,如下:
void thread_task1(vector<int> &v){
m.lock();
cout << "lock_guard get m from thread 1" << endl;
v[0] = 1;
if (v[1] == 2) {
cout << "from thread 1: " << v[0] << endl;
}
m.unlock();
}
这样即便不能满足条件,线程t1也会unlock。