4 死锁
4.1 死锁概念
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
一个锁,也可以产生死锁问题,比如一个线程申请完锁后,又申请锁,此时他就会带着锁阻塞,其它线程也因为得不到锁而阻塞。
4.2 死锁的4个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
4.3 避免死锁
- 破坏死锁的四个必要条件,任意一个即可
- 加锁顺序一致,破坏循环等待条件
- 避免锁未释放的场景
- 资源一次性分配
4.4 避免死锁算法
- 死锁检测算法
- 银行家算法
5. Linux 线程同步
同步问题是保证数据安全的情况下,让线程访问资源具有一定的顺序性,从而有效避免饥饿问题。
5.1 条件变量
- 当一个线程互斥地访问某个变量时,它可能发现在 正在访问临界资源的线程 改变状态之前,它什么也做不了。
- 例如当一个线程尝试访问临界资源时,资源已被其它线程使用,它只能等待,假设这里有一个
task_struct* wait_queue
和一个铃铛🔔
该未能访问资源的线程就会push()
进这个队列中,期间再来新的线程,如果资源未就绪,也会push()
到这个队列中。
当线程访问完临界资源后,会敲一下🔔,可以选择通知某一个线程或者所有线程,让该线程访问临界资源,
然后该线程选择退出或者继续去队尾等待。我们把这个🔔和队列合在一起,成为条件变量 - 所以条件变量需要提供 通知线程的机制 和一个 等待队列
- 注意每一个新来的线程都是会先尝试访问临界资源(申请锁),发现有线程正在使用,于是就去等待队列中去等待了。所以条件变量需要配合互斥锁使用,目的是为了保证数据安全。
5.2 条件变量函数
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL // 暂时不关心,设置为NULL
// 也可以是静态函数
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
销毁,如果用静态函数则不需要
int pthread_cond_destroy(pthread_cond_t *cond)
加入等待队列
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量
pthread_cond_wait()
用于阻塞当前线程,等待别的线程使用pthread_cond_signal()
或pthread_cond_broadcast()
来唤醒它。pthread_cond_wait()
必须与pthread_mutex_t
配套使用pthread_cond_wait()
函数一进入wait状态就会自动release mutex。当其他线程通过pthread_cond_signal()
或pthread_cond_broadcast()
,把该线程唤醒,使pthread_cond_wait()
通过(返回)时,该线程又自动获得该mutex。
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond); // 全部唤醒
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒一个
5.3 一个案例
#define NUM 5
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 想要让多线程依次访问该临界资源
int cnt = 0;
// 自增cnt
void* AddCount(void* args)
{
pthread_detach(pthread_self());
int num = (size_t)args;
printf("thread %d creat sucess\n", num);
while(true) {
pthread_mutex_lock(&mutex);
/*pthread_cond_wait()会自动释放该线程的锁*/
pthread_cond_wait(&cond, &mutex);
printf("Thread-%d, cnt: %d\n", num, cnt++);
pthread_mutex_unlock(&mutex);
}
}
int main()
{
for(size_t i = 0;i < NUM; ++i) {
pthread_t tid;
/*注意这里不能传(void*)&i,和主线程共享同一个i。因为不清楚主线程和子线程哪一个先运行。
如果主线程跑的很快,直接把i递增到了4,那么此时所有的线程的num就是4了,这就出现了并发问题*/
pthread_create(&tid, nullptr, AddCount, (void*)i);
usleep(1000); // 保证顺序性,让结果好观察
}
sleep(3);
printf("main thread start control\n");
while(true) {
sleep(1);
pthread_cond_signal(&cond); // 唤醒在cond等待队列下等待的一个线程,默认第一个
}
return 0;
}
如果第35行改为
pthread_cond_broadcast(&cond);
这时,线程就不会一个一个被唤醒,而是Thread-0到Thread-4一下全部被唤醒,打印出来
虽然16行直接让该线程去等待队列等待了。但实际上大部分情况都是因为临界资源不就绪,进而线程去等待
由于需要判断临界资源是否就绪,也是访问临界资源,所以需要在加锁之后,这样才能保证安全。