死锁在多线程中是非常经典,常见的现象。那么在这一篇中为什么要学习死锁,死锁可能会带来什么坏处?其实,了解死锁现象不是让我们去写死锁这样的程序,而是在了解死锁现象后,怎么在程序中避免出现死锁现象
。正所谓知己知彼,百战不殆,说的就是这个道理。
好了,现在来看一个最简单的死锁问题,一个线程试图对同一个互斥量加锁两次:
线程1拿到锁后,调用pthread_mutex_lock进行加锁成功,然后线程1闲着无聊又调用了pthread_mutex_lock加锁,注意,这时线程1还没有释放锁,所以线程1第二次加锁会失败并阻塞在这,等待解锁唤醒自己,但是线程1阻塞后又没有办法释放锁,因此线程1就陷入了一个死循环,无限期的阻塞等待下去,从而造成死锁现象。
一个简单的死锁示例代码
#include
#include
#include
#include
#include
#include
#include
//定义互斥锁
pthread_mutex_t mutex;
//线程主控函数
void *tfn(void *arg){
//第一次加锁
int ret = pthread_mutex_lock(&mutex);
if(ret == 0){
printf("lock succes --- 1\n");
}else{
printf("lock failed --- 1\n");
}
printf("hello world\n");
//第二次加锁
ret = pthread_mutex_lock(&mutex);
if(ret == 0){
printf("lock success --- 2\n");
}else{
//加锁失败则会阻塞,不会打印lock failed --- 2
printf("lock failed --- 2\n");
}
return NULL;
}
int main(void) {
pthread_t tid;
//初始化互斥锁
pthread_mutex_init(&mutex , NULL);
//创建线程
int ret = pthread_create(&tid, NULL, tfn, NULL);
if(ret != 0){
fprintf(stderr , "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
//回收线程
pthread_join(tid , NULL);
//销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
第一次加锁成功并打印lock succes,然后打印hello world,第二次加锁会失败,并阻塞在此,无法打印lock failed — 2,从而出现死锁现象。但是要注意的是,死锁并不是一种锁,而是一种会导致程序出现错误的现象。
其实除了第一小节的方式会出现死锁现象,还有其他方式也可能会导致死锁现象发生,比如下面这种方式。
如图所示,按照规则:每个线程想要访问2个共享数据必须拿到A锁和B锁才能操作。
假如现在A线程拿到了A锁,B线程拿到了B锁,当A线程调用lock请求B锁时,因为B锁已被B线程掌握,但是B线程还没有释放B锁,所以A线程会请求失败阻塞等待。同理,当B线程调用lock去请求A锁时,因为A线程没还有释放A锁,所以B线程会请求失败阻塞等待。
这时A线程和B线程都在等待对方先释放,但是谁也不想先释放,双方会一直死等下去,这也是一种死锁现象。
#include
#include
#include
#include
#include
#include
#include
//定义A锁和B锁
pthread_mutex_t mutex_A , mutex_B;
//线程主控函数
void *tfn(void *arg){
int i = (int)arg;
int ret;
//A线程对A锁加锁
if(i == 0){
ret = pthread_mutex_lock(&mutex_A);
if(ret == 0){
printf("pthread_A lock A succes\n");
}else{
printf("pthread_A lock A failed\n");
}
}
//B线程对B锁加锁
if(i == 1){
ret = pthread_mutex_lock(&mutex_B);
if(ret == 0){
printf("pthread_B lock B succes\n");
}else{
printf("pthread_B lock B failed\n");
}
}
//确保B线程加锁
sleep(2);
//此时A线程尝试请求B锁
if(i == 0){
printf("pthread_A is trylock B\n");
ret = pthread_mutex_lock(&mutex_B);
if(ret == 0){
printf("pthread_A lock B succes\n");
}else{
printf("pthread_A lock B failed\n");
}
}
//此时B线程尝试请求A锁
if(i == 1){
printf("pthread_B is trylock A\n");
ret = pthread_mutex_lock(&mutex_A);
if(ret == 0){
printf("pthread_B lock A succes\n");
}else{
printf("pthread_B lock A failed\n");
}
}
//只要任意线程拿到A锁和B锁就打印hello world
if(i == 0){
printf("pthread_A print hello world\n");
}else{
printf("pthread_B print hello world\n");
}
return NULL;
}
int main(void) {
pthread_t tid[2];
//初始化A锁和B锁
pthread_mutex_init(&mutex_A , NULL);
pthread_mutex_init(&mutex_B , NULL);
//创建2个线程,循环创建多个线程时注意传参问题
int i;
for(i = 0; i < 2; i++){
int ret = pthread_create(&tid[i] , NULL, tfn, (void *)i);
if(ret != 0){
fprintf(stderr , "pthread_create error: %s\n", strerror(ret));
exit(-1);
}
}
//回收线程
pthread_join(tid[0] , NULL);
pthread_join(tid[1] , NULL);
//销毁互斥锁
pthread_mutex_destroy(&mutex_A);
pthread_mutex_destroy(&mutex_B);
return 0;
}
从程序执行结果来看,A线程成功拿到A锁,B线程成功拿到B锁,如果A线程对B锁尝试加锁会失败并阻塞,同理,B线程对A锁尝试加锁也会失败并阻塞,然后A,B两个线程都在死等对方释放锁。
说了这么多,那有没有什么方法可以避免出现死锁现象呢?要避免死锁问题,最简单的定义加锁顺序:
如上图所示,A线程和B线程以相同的顺序进行加锁,比如A线程对A锁进行加锁,然后再对B锁进行加锁,这样才能操作共享资源。B线程同理,这样就可以避免死锁问题了。
另一种方案参考死锁现象二,就是说A线程调用pthread_mutex_lock函数先对A锁进行加锁,然后再调用pthread_mutex_trylock对其他线程进行加锁。如果任意线程调用pthread_mutex_trylock加锁失败(返回EBUSY错误),那么该线程则释放所持有的锁,然后经过一段时间再重新加锁。这种方式与前者相比,效率相对较低,因为线程可能要经过多次循环才有可能加锁成功,但是从另一方面来讲,这种方式的灵活性较高,无需受制于加锁顺序。
总结:死锁并不是锁的一种,而是一种现象