登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  php教程

PHP curl_multi 怎样并发请求并读取完成队列

来源:17golang原创

时间:2026-11-02 10:30:26 188浏览 收藏

我第一次把一批接口改成并发请求时,最容易混淆的不是“怎样同时发出去”,而是“到底哪个请求已经完成”。PHP 的 curl_multi 需要把多个单请求句柄交给同一个 multi handle,再由 curl_multi_exec() 推进传输;真正的完成通知,则要通过 curl_multi_info_read() 一条条取出。

官方地址:https://www.php.net/

记住两个状态:still_running 说明还有多少句柄处于活动状态,curl_multi_info_read() 说明当前完成消息队列里还有什么。前者决定是否继续驱动,后者决定读取哪个响应和错误。

curl_multi 到底解决了什么问题

单个 curl_exec() 适合一次请求一次处理;当多个 HTTP 请求互不依赖时,把它们全部加入 CurlMultiHandle,可以让 cURL 统一管理这些连接。这里的“并发”不等于把 PHP 代码复制成多个线程,而是由 cURL multi 接口同时推进多个传输。

我现在会先把对象关系画清楚:curl_multi_init() 创建容器;每个地址通过 curl_init() 得到自己的 CurlHandle;curl_multi_add_handle() 把单个句柄登记到容器;curl_multi_exec() 负责推进。只有设置了 CURLOPT_RETURNTRANSFER,后面才适合用 curl_multi_getcontent() 取回响应体。

CurlMultiHandle、CurlHandle 与 curl_multi_exec 的静态关系说明图
图1:PHP curl_multi 多句柄与单请求句柄的静态关系说明图,不是截图,也不是本机运行证据。

先为每个地址准备自己的 CurlHandle

不要只保存 URL 字符串。完成消息返回的是具体句柄,所以我通常会同时保存句柄与 URL 的映射,这样消息到来时能知道它对应哪一个业务请求。下面的示例故意把请求配置、映射和资源清理放在一起,便于改成自己的业务封装。

 'https://api.example.test/profile',
    'orders'  => 'https://api.example.test/orders',
    'quota'   => 'https://api.example.test/quota',
];

$multi = curl_multi_init();
$requests = [];

foreach ($urls as $name => $url) {
    // RETURNTRANSFER 让响应体留在句柄中,稍后交给 getcontent 读取。
    $handle = curl_init($url);
    if ($handle === false) {
        throw new RuntimeException("无法创建请求句柄:{$name}");
    }

    curl_setopt_array($handle, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 3,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_HTTPHEADER => ['Accept: application/json'],
    ]);

    // 用对象 ID 做本地映射键;完成消息会带回同一个 CurlHandle。
    $requests[spl_object_id($handle)] = [
        'name' => $name,
        'url' => $url,
        'handle' => $handle,
    ];

    $addResult = curl_multi_add_handle($multi, $handle);
    if ($addResult !== CURLM_OK) {
        curl_close($handle);
        throw new RuntimeException("加入 multi 失败:{$name}");
    }
}

这里没有把超时当成 HTTP 错误处理。连接超时、DNS 错误等属于 cURL 传输结果;即使传输成功,服务器返回 404 或 500 也仍然需要通过 curl_getinfo() 查看 HTTP 状态。

用 still_running 驱动,用 select 避免空转

调用 curl_multi_exec() 后,它会通过引用更新 $running。这个数字只适合回答“还有没有活动中的传输”,不告诉你哪一个已经完成,也不等于完成队列的长度。请求仍在进行时,可以用 curl_multi_select() 等待读写事件;如果它返回 -1,短暂休眠可以避免循环持续占用 CPU。

 0) {
        $ready = curl_multi_select($multi, 1.0);
        if ($ready === -1) {
            usleep(1000); // cURL 没有给出可等待的描述符时让出一点 CPU。
        }
    }
} while ($running > 0);

实际项目里,我更常把完成消息的读取放在每次 curl_multi_exec() 之后,而不是等 $running 变成 0 才一次性读取。这样可以边完成边消费,也能及时释放已经结束的句柄。

完成队列要用 curl_multi_info_read 排空

curl_multi_info_read() 返回一条完成消息,读完后要继续调用,直到返回 false,这表示当前时刻的消息已经取空。消息中的 handle 指向完成的请求,result 是 cURL 传输结果;当 msg 为 CURLMSG_DONE 时,才进入响应体和 HTTP 状态处理。

curl_multi_exec、still_running 与 curl_multi_info_read 完成消息的静态关系说明图
图2:PHP curl_multi 完成消息与结果读取的静态关系说明图,不是截图,也不是运行结果。
 'unknown', 'url' => ''];
        $httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);

        if ($info['result'] === CURLE_OK) {
            // 传输成功不代表 HTTP 一定是 2xx,状态码要单独判断。
            $results[$meta['name']] = [
                'url' => $meta['url'],
                'http_code' => $httpCode,
                'body' => curl_multi_getcontent($handle),
            ];
        } else {
            // result 是 cURL 传输错误,例如超时、解析失败或 TLS 失败。
            $results[$meta['name']] = [
                'url' => $meta['url'],
                'http_code' => $httpCode,
                'error' => curl_strerror($info['result']),
            ];
        }

        // 先读取需要的数据,再移除并关闭这个已经完成的句柄。
        curl_multi_remove_handle($multi, $handle);
        curl_close($handle);
        unset($requests[$key]);
    }

    if ($running > 0) {
        $ready = curl_multi_select($multi, 1.0);
        if ($ready === -1) {
            usleep(1000); // 处理 select 的异常返回,避免忙等。
        }
    }
} while ($running > 0);

curl_multi_close($multi);

如果你发现“请求都结束了但结果数组为空”,优先检查是否漏掉了 curl_multi_info_read() 循环,或者是否没有设置 CURLOPT_RETURNTRANSFER。如果只判断 $running,你只能知道活动句柄变少了,不能拿到已完成句柄的业务结果。

我会重点检查的三个边界

现象应该看什么常见误区
整个 multi 调用失败curl_multi_exec() 的返回值把它当成某一个 URL 的 HTTP 状态
某个请求传输失败curl_multi_info_read() 返回消息里的 result只看 curl_getinfo() 的状态码
服务器返回 404/500curl_getinfo($handle, CURLINFO_HTTP_CODE)把非 2xx 当成 cURL 传输异常
循环 CPU 占用异常curl_multi_select() 的等待逻辑无条件高速调用 curl_multi_exec()

PHP 8 中相关函数参数和返回值使用 CurlHandle、CurlMultiHandle 对象;较早版本使用 resource。代码结构仍然是“初始化、加入、驱动、读取消息、移除、关闭”,但项目若要兼容旧 PHP,应该把实际版本和 cURL 扩展能力纳入部署检查。

适合什么时候用 curl_multi

它适合一批彼此独立、数量可控、需要一次请求周期内合并处理的 HTTP 调用,例如读取多个服务的概览数据。它不自动提供业务限流、重试策略、熔断、优先级或任务持久化;请求数量很大时,我会在外层分批加入句柄,并记录每个请求的超时和失败原因。

我自己的判断是:如果只是连续调用两三个接口,单句柄代码可能更直观;当等待时间明显来自多个相互独立的网络请求时,curl_multi 才真正值得引入。关键不是追求“越多句柄越快”,而是让完成消息、HTTP 状态和资源清理都保持可追踪。

相关问题

  • curl_multi_info_read() 返回 false 是错误吗? 不是,它表示当前完成消息已经读空;只要 $running 仍大于 0,就继续等待并驱动。
  • curl_multi_exec() 返回 CURLM_OK 就代表所有请求成功吗? 不代表。它只说明 multi 栈本身没有报错,单请求结果仍要看消息里的 result。
  • 为什么拿不到响应体? 检查每个句柄是否设置 CURLOPT_RETURNTRANSFER => true,并在移除句柄前调用 curl_multi_getcontent()。
  • 是否应该把 HTTP 500 当作 cURL 错误? 不应该。传输成功但 HTTP 状态异常时,应把状态码作为业务失败单独处理。

参考入口:https://www.php.net/manual/en/function.curl-multi-exec.php、https://www.php.net/manual/en/function.curl-multi-info-read.php、https://www.php.net/manual/en/function.curl-multi-add-handle.php。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>