PHP curl_multi 怎样并发请求并读取完成队列
我第一次把一批接口改成并发请求时,最容易混淆的不是“怎样同时发出去”,而是“到底哪个请求已经完成”。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。
PHP Composer 平台配置怎样锁定运行时版本
- 上一篇
- PHP Composer 平台配置怎样锁定运行时版本
- 下一篇
- PHP readonly 类怎样避免配置对象被意外修改
-
- 文章 · php教程 | 1小时前 |
- PHP readonly 类怎样避免配置对象被意外修改
- 200浏览 收藏
-
- 文章 · php教程 | 1小时前 | 依赖管理 · PHP · php PHP扩展 Composer PHP版本 composer.lock config.platform check-platform-reqs
- PHP Composer 平台配置怎样锁定运行时版本
- 185浏览 收藏
-
- 文章 · php教程 | 1小时前 |
- PHP Generators 怎样把数据库结果转成惰性序列
- 496浏览 收藏
-
- 文章 · php教程 | 1小时前 |
- PHP str_increment 怎样生成连续的字母数字编号
- 356浏览 收藏
-
- 文章 · php教程 | 1小时前 | PHP · php iconv stream_filter_append stream_copy_to_stream
- PHP stream_filter_append 怎样增量转换大文件
- 398浏览 收藏
-
- 文章 · php教程 | 1小时前 |
- PHP Fiber 怎样把协作式任务接入事件循环
- 135浏览 收藏
-
- 文章 · php教程 | 1小时前 |
- PHP 枚举怎样给数据库值提供安全映射
- 424浏览 收藏
-
- 文章 · php教程 | 1小时前 | php教程 · 元数据 属性 Attributes PHP 8 ReflectionClass ReflectionMethod ReflectionAttribute
- PHP 8 属性怎样用反射读取自定义元数据
- 395浏览 收藏
-
- 文章 · php教程 | 1小时前 |
- PHP PDO 怎样设置 ATTR_EMULATE_PREPARES 避免类型误判
- 203浏览 收藏
-
- 文章 · php教程 | 1天前 |
- PHP Fiber 中异常抛出与 finally 清理顺序
- 441浏览 收藏
-
- 文章 · php教程 | 1天前 | 性能 · PHP · php 大文件传输 stream_set_chunk_size
- PHP stream_set_chunk_size 对大文件传输的缓冲影响
- 288浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 842次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 842次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 921次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 897次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 725次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- Go保证并发安全底层实现详解
- 2023-02-24 417浏览
-
- Go语言开发保证并发安全实例详解
- 2023-01-07 328浏览
-
- Golang 手写一个简单的并发任务 manager
- 2022-12-23 367浏览
-
- Go语言使用goroutine及通道实现并发详解
- 2023-01-02 221浏览

