过程
前几天使用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 ;
}
运行速度超级快,永远比人的手速要快,大功告成~~~~