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() 取回响应体。

先为每个地址准备自己的 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 状态处理。

'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/500 | curl_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。
-
200 收藏
-
文章 · php教程 | 1小时前 | 依赖管理 · PHP · php PHP扩展 Composer PHP版本 composer.lock config.platform check-platform-reqs185 收藏
-
413 收藏
-
496 收藏
-
356 收藏
-
398 收藏
-
135 收藏
-
424 收藏
-
文章 · php教程 | 1小时前 | php教程 · 元数据 属性 Attributes PHP 8 ReflectionClass ReflectionMethod ReflectionAttribute395 收藏
-
203 收藏
-
441 收藏
-
288 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习