当前位置:首页 > 文章列表 > 文章 > php教程 > PHP stream_select 怎么同时监听多个子进程管道

PHP stream_select 怎么同时监听多个子进程管道

来源:17golang原创 2026-11-05 13:51:30 0浏览 收藏

PHP 同时读取多个子进程输出时,不要把某一条管道写成阻塞式的“读完再读下一条”。更稳妥的做法是用 proc_open() 保存每个子进程的 stdout 管道,把当前仍然活跃的管道复制到 stream_select(),只读取本轮已经就绪的流;管道到达 EOF 后立即关闭并从主表移除。

关键点只有一个:stream_select() 会原地改写传入的 read 数组,所以主监听表不能直接复用,必须每轮重新构造临时数组。

官方地址:https://www.php.net/manual/en/function.stream-select.php

下面用两个短命令模拟两个子进程,示例只聚焦 PHP CLI 下的 stdout 管道、超时和资源回收。

先把多个子进程变成可管理的管道集合

proc_open() 会把描述符数组里的管道端返回到 $pipes。父进程要保存三类信息:进程句柄、可读的 stdout 管道,以及管道对应的任务名。这样读到一段文本时,才能知道它来自哪个子进程。

父进程、两个子进程、stdout 管道和任务标签映射的静态结构说明图
图1:结构说明图,查看父进程如何分别持有多个子进程的 stdout 管道。

下面的 spawn() 只打开 stdout 读管道;子进程的 stderr 在真实项目中也应单独处理,避免错误输出堵住进程。

 ['pipe', 'r'], // 父进程不向 stdin 写数据。
        1 => ['pipe', 'w'], // 父进程读取子进程 stdout。
        2 => ['pipe', 'w'], // 单独保留 stderr,生产环境应持续消费它。
    ];
    $pipes = [];
    $process = proc_open($command, $descriptorSpec, $pipes);
    if (!is_resource($process)) {
        throw new RuntimeException("无法启动 {$name}");
    }

    // 不需要输入时立即关闭 stdin,避免子进程等待 EOF。
    fclose($pipes[0]);
    stream_set_blocking($pipes[1], false); // 读取时不因单条管道卡住整个监控器。
    stream_set_blocking($pipes[2], false); // stderr 也切到非阻塞,便于后续扩展。
    return ['name' => $name, 'process' => $process, 'stdout' => $pipes[1], 'stderr' => $pipes[2]];
}

$jobs = [spawn('任务 A', 180), spawn('任务 B', 420)];
$active = [];
foreach ($jobs as $job) {
    // 用资源 ID 作为主键,避免不同管道对象混在一起。
    $active[(int) $job['stdout']] = $job;
}
?>

监听循环为什么要重建 read 数组

stream_select($read, ...) 返回后,$read 只保留已经就绪或已经到 EOF 的流,原数组会被修改。若把它当成永久监听表,下一轮就可能漏掉仍在工作的管道。因此主表保存在 $active,每轮用 array_values() 生成新的 read 数组。

活跃管道表、stream_select 临时就绪数组、读取和 EOF 清理的静态结构说明图
图2:结构说明图,查看监听数组副本与 EOF 清理如何回写活跃管道表。
 $job['stdout'], array_values($active));
    $write = null;
    $except = null;

    // 2 秒是上限,不代表一定要等满;有数据时会提前返回。
    $ready = stream_select($read, $write, $except, 2, 0);
    if ($ready === false) {
        throw new RuntimeException('stream_select 被中断或参数无效');
    }
    if ($ready === 0) {
        // 超时可以在这里做心跳、记录进度或检查取消信号。
        continue;
    }

    foreach ($read as $stream) {
        $key = (int) $stream;
        $job = $active[$key] ?? null;
        if ($job === null) {
            continue; // 管道可能在上一轮清理,避免访问已删除的状态。
        }

        $chunk = fread($stream, 8192);
        if ($chunk !== false && $chunk !== '') {
            echo '[' . $job['name'] . '] ' . rtrim($chunk) . PHP_EOL;
        }
        if (feof($stream)) {
            // EOF 表示子进程 stdout 已结束,先关管道,再从主表移除。
            fclose($stream);
            fclose($job['stderr']);
            proc_close($job['process']);
            unset($active[$key]);
        }
    }
}
?>

这里使用 === false 判断失败,因为返回值为 0 代表正常超时,不是错误。读数组中的流也可能因为 EOF 被报告为就绪,此时 fread() 得到空字符串并不意味着要继续永久监听,而是应该执行关闭和移除。

超时、读取和清理要分别处理

秒和微秒共同组成等待上限。2, 0 适合示例;如果希望由事件唤醒,可以把秒参数设为 null,但循环里就不能依赖固定时间做心跳。把两个参数都设为零会变成忙轮询,官方手册也提醒这种写法可能持续消耗 CPU。

返回或状态处理方式
false按错误处理,记录原因或退出监控循环
0超时,没有管道就绪,可做心跳后继续
大于 0遍历被修改后的 read 数组,只读取这些流
feof()关闭 stdout、stderr 和进程句柄,并从活跃表删除

如果业务只关心标准输出,stderr 也不能永远无人消费。可以把 stderr 放入另一张监听表,或者在子进程配置中把它重定向到文件;关键是保证每个资源都有明确的关闭路径。

常见问题

为什么不直接对每条管道调用 fgets?

阻塞式读取一条管道时,父进程可能一直等不到它的数据,其他已经有输出的子进程也无法及时处理。先用 stream_select() 找出可读流,才能把等待范围限制在“任意一条管道有变化”。

stream_select 的 read 数组能不能复用?

不能把它当作永久列表。函数会按本次结果原地筛选数组;正确做法是保存自己的 $active,每轮重新映射出 $read,清理 EOF 后再进入下一轮。

总结:多个子进程管道的核心不是把读取写成更复杂,而是把“活跃管道主表”和“本轮就绪副本”分开。再分别处理 false、超时、可读数据和 EOF,stream_select() 就能承担一个轻量的多管道等待器。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
CNCF 云原生 AI 应用怎样划分平台与业务边界CNCF 云原生 AI 应用怎样划分平台与业务边界
上一篇
CNCF 云原生 AI 应用怎样划分平台与业务边界
PHP SQLite3::backup 怎么把内存数据库复制到文件
下一篇
PHP SQLite3::backup 怎么把内存数据库复制到文件
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    893次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    895次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    979次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    968次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    793次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码