C语言实现多线程并发异步HTTP请求的方案与问题求解
C语言pthread并发HTTP请求实现方案与问题排查
问题背景
- 面向C语言pthread编程、HTTP请求开发新手场景,目标是实现多异步HTTP调用,达成请求并发执行效果
- 业务场景需要处理200~300个URL的批量请求,计划通过多线程并行发起请求(单线程异步模式无法充分利用硬件性能),将URL任务分发给不同线程处理,尽可能缩短整体请求耗时,避免性能瓶颈
- 公开渠道C语言相关实现资料较少,仅找到
curl_multi_*系列接口但未完全掌握用法,自行编写多线程请求代码后未达到预期性能与正确性要求,需要可落地的实现方案与学习指引
原有实现代码如下:
void *make_req(void *arguments) { CURL *curl; CURLcode res; memory_t chunk; data_t * data = (data_t*)arguments; static int i = -1; chunk.response = malloc(1); chunk.size = 0; curl = curl_easy_init(); while (i < data->nbr_sub_match) { pthread_mutex_lock(&lock); i++; pthread_mutex_unlock(&lock); curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 1<<23); // curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip, deflate"); curl_easy_setopt(curl, CURLOPT_TCP_FASTOPEN, 1L); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, data->header); curl_easy_setopt(curl, CURLOPT_URL, data->sub_match_urls[i]); curl_easy_setopt(curl, CURLOPT_TCP_NODELAY, 1L); /* send all data to this function */ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, cb); /* we pass our 'chunk' struct to the callback function */ curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk); curl_easy_setopt(curl, CURLOPT_USERAGENT, "libcurl-agent/1.0"); /* get it! */ res = curl_easy_perform(curl); /* check for errors */ if(res != CURLE_OK) { fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); } pthread_mutex_lock(&lock); data->sub_match_json[data->response_counter++] = chunk.response; pthread_mutex_unlock(&lock); } /* cleanup curl stuff */ curl_easy_cleanup(curl); i = -1; // return chunk.response; return NULL; } void start_threads(data_t *data) { int err, i = 0; pthread_t thread[data->nbr_threads]; curl_global_init(CURL_GLOBAL_ALL); if(pthread_mutex_init(&lock, NULL) != 0) fprintf(stderr, "failed to initialize mutex: %s\n", strerror(errno)); while (i < data->nbr_threads) { err = pthread_create(&thread[i], NULL, make_req, (void *)data); if (err != 0) { fprintf(stderr, "Error: impossible make new thread %s\n", strerror(errno)); exit(1); } i++; } // pthread_mutex_destroy(&lock); while (--i >= 0) { pthread_join(thread[i], NULL); } curl_global_cleanup(); }
原有代码的核心缺陷
- 线程安全逻辑失效:使用静态变量
i存储URL读取索引,仅对i++操作加锁,循环入口的边界判断i < data->nbr_sub_match未加锁,多线程并发时会出现索引越界、多线程重复处理同一URL、漏处理URL的问题;线程退出时将静态i重置为-1的逻辑,会直接干扰其他正在运行线程的取数流程,多次调用启动函数时该问题会100%复现。 - 内存管理错误:响应存储结构体
chunk在循环外初始化,仅在线程启动时分配一次内存,后续所有请求的响应内容都会追加写入同一块内存,导致不同URL的响应内容拼接错乱,同时存在严重内存泄漏。 - CURL句柄使用不规范:同一CURL句柄循环处理多个请求时未重置配置,上一次请求的残留参数会干扰后续请求;未设置请求超时,异常场景下会出现线程永久卡死。
- 结果存储逻辑错误:通过共享计数器
response_counter递增写入响应结果,多线程调度下响应返回顺序与URL输入顺序无法对应,后续无法完成响应与原URL的匹配。 - 资源释放遗漏:互斥锁在所有线程执行完成后未销毁,存在资源泄漏。
修正实现方案
方案1:pthread + libcurl easy 接口(逻辑简单易理解)
核心修正思路:每个线程独立持有CURL句柄,共享任务索引的读取、判断、写入全程加锁,每次请求单独分配响应内存,结果按URL索引直接存入数组避免顺序错乱。
首先定义公共结构体:
typedef struct { char **sub_match_urls; int nbr_sub_match; struct curl_slist *header; char **sub_match_json; int url_index; // 替换原静态变量i,存在共享结构体中 pthread_mutex_t lock; int nbr_threads; } data_t; typedef struct { char *response; size_t size; } memory_t; // 响应写回调 static size_t cb(void *contents, size_t size, size_t nmemb, void *userp) { size_t realsize = size * nmemb; memory_t *mem = (memory_t *)userp; char *ptr = realloc(mem->response, mem->size + realsize + 1); if(!ptr) return 0; mem->response = ptr; memcpy(&(mem->response[mem->size]), contents, realsize); mem->size += realsize; mem->response[mem->size] = 0; return realsize; }
修正后的线程处理函数:
void *make_req(void *arguments) { CURL *curl = curl_easy_init(); CURLcode res; data_t *data = (data_t*)arguments; if(!curl) return NULL; // 初始化线程通用CURL配置 curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 1<<23); curl_easy_setopt(curl, CURLOPT_TCP_FASTOPEN, 1L); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, data->header); curl_easy_setopt(curl, CURLOPT_TCP_NODELAY, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, cb); curl_easy_setopt(curl, CURLOPT_USERAGENT, "libcurl-agent/1.0"); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip, deflate"); while (1) { int cur_idx; // 取任务全程加锁,避免竞态 pthread_mutex_lock(&data->lock); cur_idx = data->url_index; data->url_index++; pthread_mutex_unlock(&data->lock); if (cur_idx >= data->nbr_sub_match) break; // 每次请求单独初始化响应内存 memory_t chunk = { .response = malloc(1), .size = 0 }; if(!chunk.response) continue; curl_easy_setopt(curl, CURLOPT_URL, data->sub_match_urls[cur_idx]); curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk); res = curl_easy_perform(curl); if(res != CURLE_OK) { fprintf(stderr, "请求URL %s 失败: %s\n", data->sub_match_urls[cur_idx], curl_easy_strerror(res)); free(chunk.response); chunk.response = NULL; } // 按索引直接存入结果数组,保证顺序对应 pthread_mutex_lock(&data->lock); data->sub_match_json[cur_idx] = chunk.response; pthread_mutex_unlock(&data->lock); // 重置句柄,清除本次请求残留配置 curl_easy_reset(curl); // 重新加载通用配置 curl_easy_setopt(curl, CURLOPT_BUFFERSIZE, 1<<23); curl_easy_setopt(curl, CURLOPT_TCP_FASTOPEN, 1L); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, data->header); curl_easy_setopt(curl, CURLOPT_TCP_NODELAY, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, cb); curl_easy_setopt(curl, CURLOPT_USERAGENT, "libcurl-agent/1.0"); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip, deflate"); } curl_easy_cleanup(curl); return NULL; }
修正后的线程启动函数:
void start_threads(data_t *data) { int err, i = 0; pthread_t thread[data->nbr_threads]; // 初始化共享状态 data->url_index = 0; curl_global_init(CURL_GLOBAL_ALL); if(pthread_mutex_init(&data->lock, NULL) != 0) { fprintf(stderr, "互斥锁初始化失败: %s\n", strerror(errno)); exit(1); } while (i < data->nbr_threads) { err = pthread_create(&thread[i], NULL, make_req, (void *)data); if (err != 0) { fprintf(stderr, "创建线程失败: %s\n", strerror(errno)); exit(1); } i++; } for (int j = 0; j < data->nbr_threads; j++) { pthread_join(thread[j], NULL); } pthread_mutex_destroy(&data->lock); curl_global_cleanup(); }
方案2:curl_multi 异步接口实现(性能更优)
如果不想手动管理多线程,可以直接使用libcurl提供的curl_multi_*系列事件驱动接口,单线程即可同时处理上百个并发HTTP请求,无需考虑线程安全、锁竞争问题,整体性能优于多线程easy接口方案,更适合200~300URL的批量请求场景。核心逻辑为:将所有初始化好的CURL easy句柄添加到multi栈中,通过curl_multi_poll监听网络事件,批量处理已完成的请求,直到所有请求处理完成即可。学习时可以直接参考libcurl提供的官方demo代码,逻辑门槛不高。
性能优化注意事项
- 线程数无需设置过高:HTTP请求属于IO密集型任务,处理200300个URL时设置816个工作线程即可,线程数过高会导致频繁上下文切换、TCP连接数超限、触发目标站点限流,反而降低整体性能
- 必须添加超时配置:统一设置连接超时、总请求超时,避免个别异常请求卡死导致整个任务无法结束
- 结果存储建议直接按URL索引写入数组,不要用递增计数器存储,从根源上避免响应和原URL无法匹配的问题
- 所有跨线程共享的变量读写操作(包括索引读取、结果写入、状态更新)必须加锁保护,不要遗漏边界判断逻辑
- 可以开启长连接复用、gzip压缩支持进一步提升请求速度
内容的提问来源于stack exchange,提问作者federikowsky
相关产品推荐
相关产品推荐

