• C后端开发,记录一个关于条件变量的死锁bug


    过程

    前几天使用IPC进程通信的原理写了一个聊天室,并且支持高并发。

    在对于预防共享内存被多个进程同时使用导致信息丢失的情况时,使用了互斥锁,在客户端的代码中,最一开始是这样的:

    void send_by_signal() {
        DBG("DBG: send_by_signal...\n");
        char buff[MAX_NAME_LENGTH] = {0};
        while(1) {
            int ret = scanf("%[^\n]", buff);
            getchar();
            if (ret == 0) continue;
            // 上锁
            pthread_mutex_lock(&share_memory->mutex);
            strcpy(share_memory->name, name);
            strcpy(share_memory->message, buff);
            pthread_mutex_unlock(&share_memory->mutex);
            // 解锁
            kill(server_pid, SIGUSR1);
            DBG(GREEN"%s"NONE"->"YELLOW"%s\n"NONE, share_memory->name, share_memory->message);
        }
    
        return ;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    但是在高并发测试之后发现,上面的代码具有这样的问题。当解锁后,下一个拿到共享内存的不一定是服务端,有可能被其他的客户端拿到,然后对其中的数据造成覆盖,从而数据丢失,因此为了解决这个问题,需要对共享内存中的数据是否被“消耗”而做出判断。

    由此改成了下面的代码:

    void send_by_signal() {
        DBG("DBG: send_by_signal...\n");
        char buff[MAX_NAME_LENGTH] = {0};
        while(1) {
            int ret = scanf("%[^\n]", buff);
            getchar();
            if (ret == 0) continue;
            int lock_before_strcpy_flag;
            do {
                pthread_mutex_lock(&share_memory->mutex);
                lock_before_strcpy_flag = 0;
                // 如果有数据
                if (strlen(share_memory->message)) {
    		            // 解锁之后
                    pthread_mutex_unlock(&share_memory->mutex);
                    lock_before_strcpy_flag = 1;
                }
            } while(lock_before_strcpy_flag);
            strcpy(share_memory->name, name);
            strcpy(share_memory->message, buff);
            pthread_mutex_unlock(&share_memory->mutex);
            kill(server_pid, SIGUSR1);
            DBG(GREEN"%s"NONE"->"YELLOW"%s\n"NONE, share_memory->name, share_memory->message);
        }
        return ;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    使用了一个 do-while 循环,当客户端拿到共享内存之后,会对其中的数据进行检测,如果存在数据,说明数据还未被消耗,这个时候不能写数据,因此需要再解锁继续等待,基本算是解决了高并发的问题。

    之后学习了condvar,发现只要从服务端发出可以输入消息的notify,通知客户端进程,就可以解决“未消耗的问题”,因此写出了以下代码:

    server :

    ...
    void print(int signum) {
        DBG("Received a signal...");
        pthread_mutex_lock(&share_memory->mutex);
        if (strlen(share_memory->message) == 0) {
            pthread_mutex_unlock(&share_memory->mutex);
            return ;
        }
        printf("<%s> : %s\n", share_memory->name, share_memory->message);
        memset(share_memory->message, 0, MAX_MSG);
        pthread_mutex_unlock(&share_memory->mutex);
        // 在这里添加了发送条件变量的函数,表示数据已经被发送出去了,可以接收新数据了
        pthread_cond_signal(&share_memory->cond);
    }
    ...
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    client :

    ...
    void send_by_signal() {
        DBG("DBG: send_by_signal...\n");
        char buff[MAX_NAME_LENGTH] = {0};
        while(1) {
            int ret = scanf("%[^\n]", buff);
            getchar();
            if (ret == 0) continue;
            pthread_mutex_lock(&share_memory->mutex);
            // 在这里添加了等待condvar的函数
            // 如果没有收到信号,说明有数据没有被发送,那么就阻塞并且等待notify
            pthread_cond_wait(&share_memory->cond, &share_memory->mutex);
            strcpy(share_memory->name, name);
            strcpy(share_memory->message, buff);
            pthread_mutex_unlock(&share_memory->mutex);
            kill(server_pid, SIGUSR1);
            DBG(GREEN"%s"NONE"->"YELLOW"%s\n"NONE, share_memory->name, share_memory->message);
        }
        return ;
    }
    ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    但是运行之后发生了死锁,经过排查之后,发现因为我的 print 函数如果想要触发,是需要收到notify的,但是在 client 端,程序被阻塞到了 pthread_cond_wait 这里,因此造成了死锁。

    解决方案就是,在这个函数上套上一个 if 语句:

    if (strlen(share_memory->message) != 0) {
    		pthread_cond_wait(&share_memory->cond, &share_memory->mutex);
    }
    
    • 1
    • 2
    • 3

    client 拿到共享内存后,首先检查共享内存中是否有数据,如果有,说明有数据未被读出,就进入等待notify的状态,如果没有,说明可以存入数据,直接跳过等待函数,输入数据。这个时候其他的 client 都会检测到数据存在,因此都会进入等待状态,这个时候 server 端收到信号执行 print 函数,之后输出条件信号。

    本以为能够解决问题,但是还是忽略了一点,当 server 端输出条件信号时,发现出现了虚假唤醒,为什么呢?

    原因是因为 if ,当一个notify出现时,所有的 client 端都会被这个notify唤醒,但是只有一个能够拿到condvar,因此会出现一个 client 已经输入数据,但是另一个 client 也跳出wait状态,覆写数据的情况,仍然会造成数据丢失。因此 if 是不够的,需要换成 while

    最终的代码:

    void send_by_signal() {
        DBG("DBG: send_by_signal...\n");
        char buff[MAX_NAME_LENGTH] = {0};
        while(1) {
            int ret = scanf("%[^\n]", buff);
            getchar();
            if (ret == 0) continue;
            pthread_mutex_lock(&share_memory->mutex);
            while (strlen(share_memory->message) != 0) {
                pthread_cond_wait(&share_memory->cond, &share_memory->mutex);
            }
            strcpy(share_memory->name, name);
            strcpy(share_memory->message, buff);
            pthread_mutex_unlock(&share_memory->mutex);
            kill(server_pid, SIGUSR1);
            DBG(GREEN"%s"NONE"->"YELLOW"%s\n"NONE, share_memory->name, share_memory->message);
        }
        return ;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    运行速度超级快,永远比人的手速要快,大功告成~~~~

  • 相关阅读:
    腾讯云轻量应用服务器搭配WordPress程序使用宝塔面板一键式搭建个人博客网站
    【Leetcode】189. 轮转数组
    uniapp websocket原生服务(自动重连、心跳检测) Ba-Websocket
    actuator--基础--02--endpoints
    kube-apiserver源码分析
    十六、Webpack常见的插件和模式
    彩色稻高食用价值 国稻种芯-何登骥:功能农业诠释农业大健康
    文本框粘贴时兼容Unix、Mac换行符的方法源码
    常用的6款Go语言Web框架
    浅谈ArrayList和LinkedList
  • 原文地址:https://blog.csdn.net/weixin_74239689/article/details/136692953