死锁
多个执行流对锁资源进行争抢访问,但是因为推进顺序不当,而导致互相等待。最终造成程序流程无法继续的情况。
举一个日常生活中的例子来说明:
有两个人一起吃饭,只有拿到两只筷子才能吃饭。但是一人只抢到一根筷子,两个人都不愿意放手,都想拿到对方的筷子,结果两个人都吃不了饭。造成死锁。
死锁产生的必要条件:
必须具备的条件,如果不具备就无法造成死锁。知道了必要条件,我们就可以预防以及避免。
- 1.互斥条件
一个锁不能大家同时加,一个人加了锁,别人就不能再加了。同一时间只有一个线程能够加锁。 - 2.不可剥夺条件
我加的锁,别人不能解锁,只有我能解锁。 - 3.请求和保持条件
吃着碗里的,看着锅里的。 线程1抢到了锁A,然后去抢锁B。抢不到锁B,也不愿意释放锁A。 - 4.环路等待条件
线程1抢到了锁A,然后去抢锁B,但是抢不到锁B。
线程2抢到了锁B,然后去抢锁A,但是抢不到锁A。
如何预防死锁:在编写代码过程中注意破坏死锁产生的必要条件即可
如何避免产生死锁:死锁检测算法和银行家算法
银行家算法:将系统的运行分成了两种状态:安全/不安全
1.当前都有哪些资源
2.哪些资源已经分配给了谁
3.现在谁都想要,都要获取哪些资源
若给你分配你想要的资源,是否会造成系统处于不安全状态。分配给一个线程想要的锁资源,是否会造成环路等待。
如果有可能造成,当前就是不安全的。分配给一个线程想要的锁资源,是否会造成环路等待。
如果有可能造成,当前就是不安全的,则不能分配。已经加的锁考虑是否释放掉。
加锁的时候采用非阻塞加锁的方式。
破坏请求与保持条件:
a.若不能加锁,则将已经加的锁释放掉。
b.要按序加锁。
同步的实现:通过条件变量实现
线程同步是指线程之间具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
条件变量:实现同步的思路向用户提供两个接口(一个是让线程陷入阻塞休眠的接口。一个是唤醒线程休眠的接口)+pcb等待队列
同步:通过条件判断(什么时候能访问资源,什么时候不能访问。若不能访问就要使线程阻塞。若能访问就要唤醒线程),实现线程对资源访问的合理性。
条件变量:只是向外提供了等待与唤醒的接口,却没有提供条件判断的功能。(条件变量本身并不具备判断什么时候该等待,什么时候该唤醒)意味着,条件判断需要用户自己来完成。
条件变量提供的接口功能:
- 1.定义条件变量
pthread_cond_t cond
- 2.初始化条件变量
pthread_coud_init(pthread_cont_t *cond,pthread_condattr_t *attr);
coud=PTHREAD_COND_INITIALIZER
- 3.若不能访问,则调用pthread_coud_wait进行等待:
pthread_coud_wait(pthread_coud_t *cond,pthread_mutex_t *mutex);
这个变量是搭配互斥锁使用的:条件变量并不提供条件判断的功能,需要用户去判断(通常条件的判断是一个临界资源的访问)。因此这个临界资源的访问,就需要受互斥锁的保护。
pthread_cond_timedwait(pthread_coud_t *cond,pthread_mutex_t *mutex,struct timespec *abstime);
限制等待时长的阻塞操作:等待一段指定的时间,时间到了调用就会保存返回–ETIMEDOUT
- 4.其他线程若是促使条件满足了,通过条件变量唤醒等待的线程:
pthread_cond_signal(pthread_coud_t *cond);
pthread_cond_signal(pthread_coud_t *cond);
- 5.若不使用条件了则销毁释放资源:
pthread_cond_destroy(pthread_coud_t *cond);
操作流程:用户不能访问资源的时候调用接口陷入等待,其他线程产生资源,然后调用接口唤醒等待队列中的线程。是否能够访问的条件判断需要用户自己完成,并且需要互斥保护。
- 1.加锁。
- 2.用户自己进行条件判断,如果不能访问,调用pthread_cond_wait陷入等待。
- 3.被唤醒之后能够访问,则访问数据,获取资源。
- 4.唤醒生产资源的线程。
- 5.解锁。
注意:
-
pthread_cond_wait中包含了三步操作(解锁+休眠+被唤醒后加锁。解锁和休眠是一个原子操作。)
-
用户自己进行条件判断需要使用while循环判断。不然会导致唤醒两个顾客:一个吃面,一个卡在锁上。吃完面的,解锁之后,应该厨师做面,但是解锁后谁都可能抢到。因此另一个顾客有可能在没有面的情况下抢到锁吃面。
-
不同的角色应该等待在不同的条件变量上,做到唤醒分明。厨师唤醒顾客队列。顾客唤醒厨师队列。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int bowl = 0; // 初始为0,表示没有面
pthread_mutex_t mutex;
pthread_cond_t consumer_cond;
pthread_cond_t cook_cond;
void *thr_customer(void *arg)
{
//这是一个顾客的流程
while(1) {
//0. 加锁操作
pthread_mutex_lock(&mutex);
while (bowl == 0)
{
//顾客加锁成功,发现没有饭,则要陷入休眠,如果没解锁就会导致厨师无法做饭
//pthread_mutex_unlock();
//如果厨师在这期间做好饭,唤醒就白唤醒了,因为顾客还没睡
//pause();//休眠等待唤醒
//pthread_mutex_lock();
//如果没饭,则要等待,因为已经加过锁了,因此等待之前要解锁,被唤醒之后要加锁
//因此pthread_cond_wait集合了三步操作:解锁/挂起/加锁
//解锁和挂起是一个原子操作-不可被打断
//顾客解锁,还没来得及挂起休眠;这时候厨师进来做饭,做好后唤醒顾客(实际顾客还没休眠)
//会导致顾客这时候拿到时间片休眠彻底卡死(厨师不会进行第二次唤醒)
pthread_cond_wait(&consumer_cond, &mutex);
}
//能走下来应该表示有饭了,bowl是等于1的
printf("It's delicious.\n");
bowl = 0; // 饭被吃完了
//唤醒厨师,再做一碗
pthread_cond_signal(&cook_cond);
//解锁操作
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *thr_cook(void *arg)
{
//这是一个厨师的流程
while(1) {
//0. 加锁操作,因为要对碗进行操作
pthread_mutex_lock(&mutex);
while (bowl == 1) {
//表示有饭,则不能做,陷入等待
pthread_cond_wait(&cook_cond, &mutex);
}
//能走下来表示没饭,则做饭 bowl=0
printf("Made a bowl of delicious rice\n");
bowl = 1;//做了一碗饭
//唤醒顾客
pthread_cond_signal(&consumer_cond);
//解锁操作
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t ctid[2];
int ret, i;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&consumer_cond, NULL);
pthread_cond_init(&cook_cond, NULL);
for (i = 0; i < 4; i++) {
ret = pthread_create(&ctid[0], NULL, thr_customer, NULL);
if (ret != 0) {
printf("create thread failed!!\n");
return -1;
}
}
for (i = 0; i < 4; i++) {
ret = pthread_create(&ctid[1], NULL, thr_cook, NULL);
if (ret != 0) {
printf("create thread failed!!\n");
return -1;
}
}
pthread_join(ctid[0], NULL);
pthread_join(ctid[1], NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&consumer_cond);
pthread_cond_destroy(&cook_cond);
return 0;
}