28-线程同步——死锁现象

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35733751/article/details/82807881

1. 死锁现象一

   死锁在多线程中是非常经典,常见的现象。那么在这一篇中为什么要学习死锁,死锁可能会带来什么坏处?其实,了解死锁现象不是让我们去写死锁这样的程序,而是在了解死锁现象后,怎么在程序中避免出现死锁现象。正所谓知己知彼,百战不殆,说的就是这个道理。


好了,现在来看一个最简单的死锁问题,一个线程试图对同一个互斥量加锁两次:
在这里插入图片描述

  线程1拿到锁后,调用pthread_mutex_lock进行加锁成功,然后线程1闲着无聊又调用了pthread_mutex_lock加锁,注意,这时线程1还没有释放锁,所以线程1第二次加锁会失败并阻塞在这,等待解锁唤醒自己,但是线程1阻塞后又没有办法释放锁,因此线程1就陷入了一个死循环,无限期的阻塞等待下去,从而造成死锁现象。


   举个简单的例子,相信大家有过出门忘带钥匙的经历,比如你觉得今天天气不错,打算和朋友出去游玩,但是把门锁上后才发现忘了带钥匙,因为此时你手里没有钥匙,所以你不能把门锁打开,也就是说,现在你只能在门外等待有钥匙的人把门打开。


2. 死锁现象一代码示例


一个简单的死锁示例代码

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <stdlib.h>

//定义互斥锁
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,从而出现死锁现象。但是要注意的是,死锁并不是一种锁,而是一种会导致程序出现错误的现象。


3. 死锁现象二


   其实除了第一小节的方式会出现死锁现象,还有其他方式也可能会导致死锁现象发生,比如下面这种方式。

在这里插入图片描述

   如图所示,按照规则:每个线程想要访问2个共享数据必须拿到A锁和B锁才能操作。

   假如现在A线程拿到了A锁,B线程拿到了B锁,当A线程调用lock请求B锁时,因为B锁已被B线程掌握,但是B线程还没有释放B锁,所以A线程会请求失败阻塞等待。同理,当B线程调用lock去请求A锁时,因为A线程没还有释放A锁,所以B线程会请求失败阻塞等待。


   这时A线程和B线程都在等待对方先释放,但是谁也不想先释放,双方会一直死等下去,这也是一种死锁现象。


4. 死锁现象二代码示例

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <stdlib.h>

//定义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线程抢到cpu
        sleep(2);

        //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");
                }
        }

        //此时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两个线程都在死等对方释放锁。


5. 避免出现死锁


   说了这么多,那有没有什么方法可以避免出现死锁现象呢?要避免死锁问题,最简单的定义加锁顺序:
在这里插入图片描述

   如上图所示,A线程和B线程以相同的顺序进行加锁,比如A线程对A锁进行加锁,然后再对B锁进行加锁,这样才能操作共享资源。B线程同理,这样就可以避免死锁问题了。


   另一种方案参考死锁现象二,就是说A线程调用pthread_mutex_lock函数先对A锁进行加锁,然后再调用pthread_mutex_trylock对其他线程进行加锁。如果任意线程调用pthread_mutex_trylock加锁失败(返回EBUSY错误),那么该线程则释放所持有的锁,然后经过一段时间再重新加锁。这种方式与前者相比,效率相对较低,因为线程可能要经过多次循环才有可能加锁成功,但是从另一方面来讲,这种方式的灵活性较高,无需受制于加锁顺序。


   总结:死锁并不是锁的一种,而是一种现象

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/82807881
28-