过程
前几天使用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的函数// 如果没有收到信号,说明有数据没有被发送,那么就阻塞并且等待notifypthread_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 ;
}
运行速度超级快,永远比人的手速要快,大功告成~~~~