C语言多线程客户端-服务器异常:请求重复与丢失问题排查
问题背景
用C语言实现了带有4个工作线程的客户端-服务器程序,采用生产者消费者模型的队列缓冲请求。启动服务器命令为./wserver -p 8003,通过测试脚本multiple_clients.bash发起15个并发请求时,出现**部分请求重复(如test13.html、test11.html)、部分请求丢失(如test7.html、test14.html)**的异常:总输出数量为15但内容不符,程序无double free崩溃,但存在单个请求被重复处理的情况。
核心代码
request.c
struct conn_args { int fd; int filename; // 类型错误:应为char* int filesize; struct conn_args *next; } /* other functions */ int add(int fd, char *filename, int filesize) { if(buffer_size >= buffer_max_size) return 1; struct conn_args *temp = malloc(sizeof(struct conn_args)); temp->fd = fd; temp->filename = filename; // 直接保存指针,未复制字符串内容 temp->filesize = filesize; temp->next = NULL; if (head == NULL) { head = temp; tail = temp; } else { tail->next = temp; tail = temp; } buffer_size++; return 0; } struct conn_args* get_args() { struct conn_args *temp = NULL; if(head!=NULL) { temp = head; head = head->next; buffer_size--; } return temp; } void *thread_request_serve_static(void *arg) { while (1) { pthread_mutex_lock(&lock); struct conn_args *arg = get_args(); if (!arg) pthread_cond_wait(&c, &lock); else { request_serve_static(arg->fd, arg->filename, arg->filesize); printf("Request for %s is removed from the buffer\n-----------------\n", arg->filename); close_or_die(arg->fd); free(arg); } pthread_mutex_unlock(&lock); } } void request_handle(int fd) { /* variable declarations and condition checks */ pthread_mutex_lock(&lock); int stat = add(fd, filename, sbuf.st_size); pthread_mutex_unlock(&lock); if (!stat) { printf("-----------------\nRequest for %s is added to the buffer\n", filename); pthread_cond_signal(&c); } else { printf("Server overload: Try again later!"); } }
wserver.c
int main(int argc, char *argv[]) { /* variable initializations and argument reading */ pthread_t thread_pool[num_threads]; for(int i=0; i<num_threads; i++) pthread_create(&thread_pool[i], NULL, thread_request_serve_static, NULL); // open the socket connection int listen_fd = open_listen_fd_or_die(port); while (1) { struct sockaddr_in client_addr; int client_len = sizeof(client_addr); int conn_fd = accept_or_die(listen_fd, (sockaddr_t *) &client_addr, (socklen_t *) &client_len); request_handle(conn_fd); } }
测试脚本 multiple_clients.bash
echo Starting requests for ((i=15;i>0;i-=1)) do ./wclient localhost 8003 /files/test$i.html & done echo Requests completed
异常原因分析
结构体成员类型错误
struct conn_args中的filename被错误定义为int类型(实际应为char*),在64位系统下,char*占8字节,int仅占4字节,会导致指针被截断,存储的地址无效或指向错误内存区域,最终显示混乱的文件名。字符串指针未复制,内存被复用覆盖
add函数中直接将传入的filename指针赋值给temp->filename,未复制字符串内容。如果request_handle中的filename是栈上变量或被复用的缓冲区,后续请求会覆盖该内存,队列中已存在的conn_args的filename会指向新的字符串,导致输出时出现重复文件名。条件变量使用逻辑缺陷
工作线程中,当队列空时调用pthread_cond_wait,但被唤醒后未重新检查队列状态,可能导致队列中的请求未被及时处理;同时未处理虚假唤醒,存在线程漏处理请求的风险。
解决方法
1. 修正结构体成员类型
将struct conn_args中的filename改为char*类型:
struct conn_args { int fd; char *filename; int filesize; struct conn_args *next; };
2. 复制文件名内容,避免指针共享
在add函数中,使用strdup分配内存并复制字符串内容:
int add(int fd, char *filename, int filesize) { if(buffer_size >= buffer_max_size) return 1; struct conn_args *temp = malloc(sizeof(struct conn_args)); if (!temp) return 1; temp->fd = fd; // 复制文件名,避免指向栈内存或复用缓冲区 temp->filename = strdup(filename); if (!temp->filename) { free(temp); return 1; } temp->filesize = filesize; temp->next = NULL; if (head == NULL) { head = temp; tail = temp; } else { tail->next = temp; tail = temp; } buffer_size++; return 0; }
3. 修正工作线程的条件变量逻辑
使用循环检查队列状态,避免虚假唤醒,同时处理请求时释放锁以提升性能:
void *thread_request_serve_static(void *arg) { while (1) { pthread_mutex_lock(&lock); // 循环检查队列是否为空,处理虚假唤醒 while (head == NULL) { pthread_cond_wait(&c, &lock); } struct conn_args *args = get_args(); pthread_mutex_unlock(&lock); if (args) { request_serve_static(args->fd, args->filename, args->filesize); printf("Request for %s is removed from the buffer\n-----------------\n", args->filename); close_or_die(args->fd); free(args->filename); // 释放复制的字符串内存 free(args); } } }
4. 补充内存释放逻辑
在工作线程中,先释放filename的内存,再释放conn_args结构体,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Tony

