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 ;
}

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

由此改成了下面的代码:

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 ;
}

使用了一个 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);
}
...

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 ;
}
...

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

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

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

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 ;
}

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

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

若亦_Royi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值