curl_multi_exec 本身不并发,仅轮询已配置的curl句柄;真并发依赖底层socket就绪、dns快速解析及无阻塞操作,需配合curl_multi_select与$active状态循环控制。

curl_multi_exec 为什么不能直接“并发”调用服务
curl_multi_exec 本身不发起并发请求,它只是轮询一批已 curl_setopt 配置好、且已 curl_multi_add_handle 加入句柄池的 cURL 资源。真正并发发生在底层 socket 层——前提是这些请求目标域名能被 DNS 快速解析、端口未被限流、且 php 没卡在阻塞操作上。
常见误操作是:在循环里反复 curl_init → curl_setopt → curl_exec,这本质是串行;而用 curl_multi_exec 却只调一次、不配合 curl_multi_select 或超时控制,结果多数请求卡在“等待响应”,看起来像没并发。
- 必须提前把所有
CURLOPT_URL、CURLOPT_RETURNTRANSFER等设好,再统一加入 multi 句柄 - 不能在
curl_multi_exec后立刻读响应——得等curl_multi_info_read返回完成状态 - DNS 解析慢会拖垮整组请求,建议开启
CURLOPT_DNS_CACHE_TIMEOUT或预解析
标准 while 循环 + curl_multi_select 的写法
这是最稳妥的并发控制模式,避免 CPU 空转,也防止某请求 hang 死整个流程。
$mh = curl_multi_init(); $handles = []; foreach ($urls as $url) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT_MS, 5000); curl_multi_add_handle($mh, $ch); $handles[] = $ch; } $active = NULL; do { $mrc = curl_multi_exec($mh, $active); if ($mrc == CURLM_CALL_MULTI_PERFORM) continue; if ($active > 0) { curl_multi_select($mh, 1); // 最多等 1 秒 } } while ($active > 0 && $mrc == CURLM_OK); // 读取结果 foreach ($handles as $ch) { $result = curl_multi_getcontent($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // 处理 $result 和 $httpCode curl_multi_remove_handle($mh, $ch); curl_close($ch); } curl_multi_close($mh);
-
curl_multi_select是关键:它让 PHP 等待任意一个 socket 就绪,而不是忙等 -
CURLOPT_TIMEOUT_MS推荐设为毫秒级,避免单个慢请求拖累全局 - 别漏掉
curl_multi_remove_handle和curl_close,否则内存泄漏
curl_multi_exec 返回值为 CURLM_OK 不代表全部完成
curl_multi_exec 返回 CURLM_OK 只表示“当前这一轮执行没有出错”,但 $active 参数才是真实进度——它返回当前仍在传输中的请求数。很多代码只检查返回值就去取结果,导致 curl_multi_getcontent 返回空或旧数据。
立即学习“PHP免费学习笔记(深入)”;
- 务必以
$active === 0作为循环退出条件,不是靠返回值 - 即使
$active > 0,也要用curl_multi_info_read主动捞已完成的句柄,避免死等 - 若需部分失败后继续,可在
curl_multi_info_read中判断CURLINFO_RESULT是否为CURLE_OK
并发数限制与连接复用的影响
PHP 默认对同一 host 最多保持 2 个 HTTP/1.1 连接(受 curl 底层限制),大量请求打向同一个域名时,后发请求会排队等待空闲连接,实际并发度远低于句柄数。
- 启用
CURLOPT_FORBID_REUSE或CURLOPT_FRESH_CONNECT会彻底禁用复用,更耗资源 - HTTP/2 支持可提升单连接并发能力,但需 PHP ≥ 7.2 + cURL ≥ 7.47.0 + OpenSSL 支持 ALPN
- 如目标服务支持,可考虑按域名分批调度,或改用
curl_multi_setopt($mh, CURLMOPT_MAXCONNECTS, 20)(注意 libcurl 版本)
真正卡点往往不在 PHP 侧,而在 DNS、TLS 握手、服务端限流或对方 Keep-Alive 策略。压测前先用 curl -w "@format.txt" -o /dev/null -s http://x 看各阶段耗时,比盲目调大并发数更有效。