You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 18:39:34