引入
今天有两个小朋友,各自拿着自己的五毛钱,一起去超市买棒棒糖吃
结果一看价格,傻眼了!
一根棒棒糖要一块钱,两个人都没办法买棒棒糖吃
这时候,其中一个小朋友对另外一个小朋友说:“把你的五毛钱给我,我买根棒棒糖吃!”
可是另外一个小朋友会答应吗?
很大概率,他心里会想着:凭什么不是你给我五毛钱,然后我买来吃呢?
于是两个人互不退让,最终都没吃到棒棒糖
这就像我们小时候常说的,三个和尚没水喝,互不退让,占有资源,于是最后谁都得不到
死锁,其实就是我们上述故事的翻版而已
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态
有一点必须要指出来,就是一个线程有可能自己发生死锁吗?
答案是可以的!
假如一个线程已经有了一把锁,还向系统OS大哥申请同一把锁,此时线程就会被挂起,一直等待,殊不知它想得到的东西,其实早就已经在它自己的手中
当然这种事件概率发生的概率其实非常低,但是并不代表不可能,作为一名程序员,什么代码都有可能写出来(bushi)
比如说下面的代码,由于线程重复申请同一把锁,于是会被挂起,陷入死锁状态
但是值得庆幸的是,主线程能够拯救它
加锁和解锁的动作可以不由同一个线程执行!
所以三秒后,主线程作为英雄,就会救下陷入自己绊倒(锁住)自己的线程
1 #include <iostream>
2 #include <pthread.h>
3 #include <cstring>
4 #include <unistd.h>
5 using namespace std;
6 pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
7 void* threadRun(void* args)
8 {
9 cout << "I am a new thread." << endl;
10 pthread_mutex_lock(&mutex);
11 cout << "I got a mutex" << endl;
12
13 pthread_mutex_lock(&mutex); //由于再次申请同一把锁,线程会被挂起
14 cout << "I alive again" << endl;
15 return nullptr;
16 }
17 int main()
18 {
19 pthread_t t1;
20 pthread_create(&t1,nullptr,threadRun,nullptr);
21
22 sleep(3);
23 cout << "main thread is running..." << endl;
24
25 pthread_mutex_unlock(&mutex);
26 cout << "I come to save you" << endl;
27
28 int n = pthread_join(t1,nullptr);
29 if(n != 0) cerr << "Error: "<< n << strerror(n)<< endl;
30
31 return 0;
32 }
结果也完美符合预期,只有线程没有被挂起,才会输出I alive again这句话,所以加锁和解锁的动作可以不由同一个线程执行这句话是对的
这里其实也可以反映锁的另外一个功能,通过锁来控制一个线程的行为
死锁的必要条件
但是,单单有了上述的概念,想要解决死锁这个问题,显然是不够的
我们要知道,究竟是什么导致了死锁?
换句话说,想要解决一个问题,先要准确描述这个问题
当然,因为什么因素而导致死锁,已经有大佬帮我们研究好了,现在我们直接学习即可
1.互斥条件:一个资源每次只能被一个执行流使用
2.请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
3 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
4.循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
对应于我们上述棒棒糖的故事,就很好理解上述的四个条件了
互斥条件
虽然棒棒糖只有一根,但是我们两兄弟互相不嫌弃,可以一起吃,那还会发生死锁的问题吗?
答案是不会!这就是互斥,线程访问的资源并非是共享的
请求与保持条件
虽然棒棒糖只有一根,但是其实其中一个小朋友对棒棒糖没有那么看重,非常大方的把自己手里仅有的五毛钱给了对方,那还会出现死锁情况吗?
答案是不会!这就是请求与保持,线程对自己已获得的资源保持不放,殊不知,有时候,退一步,海阔天空
不剥夺条件
前面的假设,都是两个小朋友都是文明人,假如其中一个长得人高马大,还爱欺负人,另外一个小朋友经常受他苦头,那很有可能,长得壮的小朋友一气之下,把对面的五毛钱抢过来,此时会出现死锁情况吗?
答案也是不会!这就是不剥夺,每个线程友好相处,大家都是文明人,不能说任意剥夺其它线程的资源
循环等待条件
最后一个条件,假如其中一个小朋友并不是只有五毛钱,而是已经有一块钱了,直接就可以买棒棒糖,此时会出现死锁情况吗?
答案是也不会!正是因为两个小朋友都循环互相等待着对方把资源给自己,才会陷入僵局;同理,正是由于线程A等待着线程B的资源,线程B又等待着线程A的资源,而两个线程永远不会释放它们的资源,才会导致死锁的出现
如何避免
所以准确描述好一个问题是什么后,解决死锁的问题,其实也就水到渠成的事情
破坏四个必要条件中的任意一个,都可以解决死锁!
到了这里,我们就可以简单再理顺一下我们的思路,以便促进我们后面的理解
首先,我们需要多线程,正是由于多线程,我们才有可能真正实现诸如边看边下载,这样非常方便的功能
但是多线程可能就会导致一个问题——并发访问临界资源
于是我们在上节提出了加锁这个办法来解决这个问题
很遗憾的是
当一个解决方案被提出的时候,往往会伴随着新的问题出现!
没有一个方案能够真正做到十全十美
而这个新的问题就是我们今天讲的死锁
可以看到其中的逻辑链是非常完备,环环相扣,无法趋近完美,只要不断解决新问题,逐渐趋近完美就足够了!
而现在我们就正式来讨论解决死锁这个问题的方法!(从四个必要条件中下手)
下面解决的方式,其实就是破坏任意一个必要条件
第一种方式,不加锁(破坏互斥条件)
那线程之间就不再互斥,可以共享资源,锁都没有,何谈死锁呢?
第二种方式,主动释放锁(破坏请求与保持条件)
线程不再一直占有资源,对面线程不久能够获取吗?这就可以解决死锁问题
第三种方式,控制线程统一释放锁(破坏不剥夺条件)
大家的资源全部都释放,等价于所有人的资源都被一个人抢走
此时大家都能自己拿自己想要的资源,这也能解决死锁问题
第四种方式,按照顺序申请锁(破坏循环等待条件)
我不再是头尾相接的循环等待了,大家排好队,依次按顺序申请锁,然后按顺序访问对应的资源,这也不会导致死锁
线程同步
概念
有了上述的四大解决方案解锁方案,我们对最后一种——按照顺序申请锁来进一步延申
还是用一个故事开头
在上篇文章中,我们介绍过学校的VIP个人自习室
聪明的你,可能已经发现有一些些不对劲
早起的鸟儿有虫吃,假如我一大早就成功预约到VIP自习室,学了一会儿
我决定还是算了,离开去吃个早餐吧,于是准备退自习室的预约
刚按下的瞬间,我又反悔了,要不再学一会?
于是我又再次预约,由于其他人并不可能每一刻都守在电脑前面预约,因此,我作为退订的人,最早知道房间为空,优先级最高,所以预约成功的可能性最大
这时候就出现一个问题了
我又预约,又退出预约,来回再那纠结反复这个行为
痛心三问:
我有干活吗?个人自习室有发挥作用吗?别人能够使用它吗?
答案显而易见,这存粹就是浪费资源
同理,假如一个线程不干活,一直在那抢锁解锁,来回反复横跳;而其它线程又无法抢到,优先级太低
这就是对资源的一种浪费!占有锁的线程来回申请锁,但并没有发挥相应的作用;其它线程又因为不占有锁,而无法访问对应的临界资源
解决这个问题的核心就在于
这个行为虽然满足我们前面提到过的申请锁的原则,但是不合理!
因此,校长就颁布一条新规则
自习完毕的人归还钥匙后,不能立即申请(对我)
在外面的人必须排好队,依次申请(对他人)
有了这个新规则,上面的问题,就可以轻松的解决
在安全的规则下,多线程访问资源具有一定的顺序性,使多线程进行协同工作,就能够合理解决上面的问题.
其中这种解决方案,我们称之为线程同步
上述这类问题被称为饥饿问题(其它线程因为优先级低的缘故等等一直抢不到资源)
条件变量
那具体如何实现多线程协同呢?
在linux系统下,用条件变量实现多线程是其中一种方式
延续我们之前的思路,解决一个问题的前提,需要先准确描述一个问题!
拿我们上节抢票来作为例子,如何解释线程同步这个现象呢?
假如现在并非抢完一千张演唱会的票后,你就没有任何机会了
还存在回流票的概念,所谓的回流票,就是有些人抢到票了,但是临时有事,或者买错了等等原因
决定放弃手上的票
因此,就算你没有抢到票,也不要轻易放弃,还是有机会!
所以,此时拿到锁的线程,判断tickets是否大于0,这个条件不满足
于是释放锁
但是有回流票啊,会有对应线程进行放票,于是为了拿到票,该线程又会立马进行加锁,由于优先级的问题,别的线程都抢不过它,甚至回流票线程都没办法放票
于是就会出现,一个线程一直在那加锁解锁,空耗资源;其它线程拿不到锁,饥肠辘辘的情况(饥饿问题)
至此,我们已经充分描述好该问题,解决问题也是水到渠成的事情
这个问题的核心就在于,一个已经持有锁的人,当临界区里面的条件不满足时,你就不应该继续再申请锁了!这并不合理(自习完毕的人归还钥匙后,不能立即申请(对我))
条件变量采用的就是这个思路解决这个问题
我们可以把条件变量先简单理解为一个结构体,而结构体内部会维护一个队列
该队列的作用就是当条件不满足的时候,申请到锁的线程,没有机会再申请锁,它会被挂起,放到我们的队列当中挂起,直到条件满足的时候,操作系统OS大哥才会把你叫出来,来抢票了!
对我(持有锁的线程)而言,我不用再来回抢锁,浪费时间
对其它线程而言,由于我被挂起,即我的锁已经被释放了,大家便有机会,继续抢锁,来获取临界资源,这便巧妙解决了饥饿问题
同步调用接口展示
说了半天,没有代码展示,只是赵括纸上谈兵而已
因此,我们迅速学习一下写个demo案例,展示一下linux下条件变量的相关接口
其中linux线程库里面提供了pthread_cond_t的结构体类型,我们将其称之为条件变量(conditon)
pthread_cond_wait()函数
两个参数:
cond:要在这个条件变量上等待
mutex:互斥量
作用:
将对应加锁的线程的锁释放,并加入到维护的队列中去,直到资源满足(wait on a condtion),再释放它出来
还有个time_wait接口,顾名思义,可以设定线程等待的时间,超过这个时间,直接不等
pthread_cond_signal()函数
一个参数:
cond:哪个条件变量
作用:
给在条件变量下等待的线程发信号,一次从等待队列里面放出来一个线程
pthread_cond_broadcast()函数
一个参数:
cond:哪个条件变量
作用:
给在条件变量下等待的线程发信号,一次从等待队列里面放出来所有线程
pthread_cond_init(),destroy()函数
一个参数:
cond:对应创建的条件变量
作用:
初始化和销毁条件变量,和锁的创建非常相似
其中init和destroy相匹配,假如直接采用全局设定,则自动销毁
代码展示:
1 #include <iostream>
2 #include <pthread.h>
3 #include <unistd.h>
4 using namespace std;
5 const int NUM = 5;
6
7 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
8 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
9 void* thread_Run(void* args)
10 {
11 string name = static_cast<const char*>(args);
12 while(true)
13 {
14 pthread_mutex_lock(&mutex);
15
16 pthread_cond_wait(&cond,&mutex);
17
18 cout << name << "is running..." <<endl;
19 pthread_mutex_unlock(&mutex);
20 }
21 return nullptr;
22 }
23 int main()
24 {
25 pthread_t tids[NUM];
26 for (int i = 0; i < NUM;i++)
27 {
28 char *buf = new char[128];
29 snprintf(buf,128,"thread-%d",i + 1);
30 pthread_create(tids + i,nullptr,thread_Run,(void*)buf);
31 }
32
33 sleep(3);
34
35 while(true)
36 {
37 cout << "main thread wake up thread" << endl;
38
39 pthread_cond_signal(&cond);
40 sleep(1);
41 }
42
43 for (int i = 0;i < NUM;i++)
44 {
45 pthread_join(tids[i],nullptr);
46 }
47
48 return 0;
49 }
对应运行的结果:
文章评论