php无真正多线程,curl_multi是唯一可靠并发方案:单线程异步非阻塞,需正确管理句柄生命周期、错误捕获与资源释放,fpm下须用try-finally确保cleanup。

PHP 里没有真正多线程,curl_multi 是唯一靠谱方案
PHP 默认是单线程同步执行的,pthread 扩展早已废弃且不兼容现代 PHP(8.0+),强行启用风险极高。所谓“多线程 curl”,实际只能靠 curl_multi 实现并发请求——它不是开多个线程,而是在单线程内复用 cURL 句柄、由 libcurl 底层调度 I/O,本质是异步非阻塞。
常见错误现象:curl_exec() 串行调用 10 次,耗时叠加;误以为 pcntl_fork() 能安全用于 Web 场景,结果 Apache/Nginx worker 崩溃或连接泄漏。
- Web SAPI(如 Apache mod_php、PHP-FPM)严禁使用
pcntl_fork(),会破坏进程模型 -
curl_multi不支持 POST 文件上传(CURLOPT_POSTFIELDS含@路径)——需改用CURLOPT_POSTFIELDS数组或CURLOPT_UPLOAD+CURLOPT_INFILE - 超时必须设
CURLOPT_TIMEOUT_MS(毫秒级),否则curl_multi_select()可能卡死
怎么写一个健壮的 curl_multi 并发请求函数
核心是控制句柄生命周期、错误捕获和资源释放。别直接套网上“封装类”,很多漏掉 curl_multi_remove_handle() 或没处理 CURLM_CALL_MULTI_PERFORM 返回值,导致请求丢包或内存泄漏。
典型使用场景:批量调用第三方 API(如查 10 个订单状态)、聚合多个微服务数据。
立即学习“PHP免费学习笔记(深入)”;
- 每次循环前用
curl_multi_add_handle()加入句柄,不要复用已执行过的句柄 - 必须检查
curl_multi_exec()返回值:CURLM_CALL_MULTI_PERFORM表示需立即再调,不是错误 - 用
curl_multi_info_read()获取完成句柄结果,而非在curl_multi_exec()后直接读curl_multi_getcontent() - 每个完成句柄务必调
curl_multi_remove_handle()+curl_close(),否则句柄堆积
while ($active && $mrc == CURLM_OK) {
if (curl_multi_exec($mh, $active) === CURLM_CALL_MULTI_PERFORM) continue;
if ($active && curl_multi_select($mh) === -1) usleep(100);
}
while ($info = curl_multi_info_read($mh)) {
$ch = $info['handle'];
$result = curl_multi_getcontent($ch);
curl_multi_remove_handle($mh, $ch);
curl_close($ch);
}
curl_multi 的性能瓶颈和绕过方法
并发数不是越高越好。libcurl 默认最大并发连接数受 CURLMOPT_MAXCONNECTS 和系统文件描述符限制,盲目设高会导致 DNS 超时、TCP 连接拒绝或目标服务限流。
真实压测中,20 并发常比 50 并发响应更快——因为大量 TIME_WAIT 连接占满本地端口,或远程服务主动关闭连接。
- 用
curl_multi_setopt($mh, CURLMOPT_MAXCONNECTS, 20)主动限流,避免打崩自己或对方 - DNS 缓存问题:加
CURLOPT_DNS_CACHE_TIMEOUT(如设为 60),否则每批请求都重新解析域名 - 若目标接口支持 HTTP/2,启用
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,复用 TCP 连接更高效 - 对失败请求别立刻重试,先
curl_getinfo($ch, CURLINFO_HTTP_CODE)判断是 4xx 还是 5xx,4xx 直接跳过
为什么 curl_multi 在 CLI 下稳定,在 FPM 下容易出错
FPM 是多进程模型,每个 worker 处理完请求后会回收资源。但若 curl_multi 句柄未彻底清理(比如异常中断没走 finally),残留句柄可能被下一个请求复用,引发 curl_error(): no handle passed 或返回上一个请求的数据。
最容易被忽略的点:FPM 配置里的 request_terminate_timeout 和 max_execution_time 不一致,导致脚本被强制 kill,curl_multi_cleanup() 根本没机会执行。
- 所有
curl_multi操作必须包在try ... finally中,finally里调curl_multi_cleanup($mh) - 避免在
register_shutdown_function()里清理 multi 句柄——FPM worker 可能已被回收,句柄无效 - CLI 脚本可用
pcntl_signal()捕获 SIGINT,但 FPM 不支持信号处理,别写这类逻辑











