当前位置:首页 > 文章列表 > 文章 > php教程 > PHP fibers 怎样把阻塞任务接入调度器

PHP fibers 怎样把阻塞任务接入调度器

来源:17golang原创 2026-11-08 00:38:05 0浏览 收藏

PHP Fibers 适合把一个正在执行的调用栈暂停下来,等外部条件满足后再从原位置继续;它本身不是事件循环,也不会自动把阻塞 I/O 变成并发任务。要把任务接入调度器,需要让 Fiber 返回一个“我在等待什么”的信号,再由调度器决定何时重新调用 resume()。

官方地址:https://www.php.net/

下面的示例只实现一个便于理解的最小调度模型:任务在 Fiber 中运行,遇到可等待点时交出控制权,调度器把它放入等待集合;条件满足后重新加入 ready 队列。它没有伪装成完整异步框架,重点是看清职责边界。

Fiber 只负责暂停恢复,调度器负责安排任务

PHP 8.1 引入的 Fiber 是完整调用栈上的可中断函数。Fiber::suspend() 可以在嵌套调用中暂停,外层通过 start()、resume() 或 throw() 重新进入。真正的任务管理仍然属于调度器:它要保存 Fiber 引用、判断状态、维护 ready 队列,并决定下一次恢复哪个任务。

PHP Fiber、调度器、ready 队列和等待适配器的职责关系说明图
图1:结构说明图,展示 PHP Fiber 与调度器的职责边界,不是运行截图。

这意味着“把阻塞任务放进 Fiber”有两个不同含义:

  • 如果任务在 Fiber 内直接调用真正会阻塞当前线程的函数,调度器仍然要等它返回,其他 Fiber 不会因此获得执行机会。
  • 如果任务把等待点包装成可恢复操作,Fiber 在等待期间主动挂起,调度器才有机会处理其他 ready 任务。

所以 Fiber 提供的是控制栈切换能力,调度器提供的是任务编排能力,非阻塞适配器则负责把外部等待转换成可通知的状态。

先把任务包装成一个可暂停的 Fiber

最小示例不直接接入网络库,而是用一个自定义的 WaitToken 表示“这个任务暂时不能继续”。任务执行到等待点后把令牌交给调度器,恢复时从 Fiber::suspend() 返回。

 blockingTask('订单读取')
);

// start() 执行到第一个 suspend(),返回 WaitToken 给外层。
$waiting = $fiber->start();

if ($waiting instanceof WaitToken) {
    // 这里由调度器保存令牌和 Fiber,而不是立刻忙等。
    echo $waiting->key, PHP_EOL;
}

注意,start() 返回的不是最终结果,而是第一次挂起点提供的值;只有 Fiber 终止后,才可以用 getReturn() 读取返回值。调度器不能把 isSuspended()、isTerminated() 等状态判断省略,否则容易重复启动或在未完成时读取结果。

用 ready 队列把恢复动作集中起来

接下来把 Fiber 和等待令牌放进一个简单的调度器。为了让示例保持可读,令牌的 ticks 只表示需要经过几次调度检查;生产环境中应替换为 socket、进程、定时器或客户端库提供的就绪事件。

 */
    private SplQueue $ready;

    /** @var array */
    private array $waiting = [];

    /** @var array */
    private array $results = [];

    public function __construct()
    {
        // ready 队列保存“现在可以继续”的 Fiber。
        $this->ready = new SplQueue();
    }

    public function add(string $name, callable $task): void
    {
        $fiber = new Fiber($task);

        // 先启动任务;它可能直接完成,也可能返回等待令牌。
        try {
            $signal = $fiber->start();
            $this->accept($name, $fiber, $signal);
        } catch (Throwable $error) {
            // 启动阶段的异常仍归属于这个任务,不能吞掉。
            $this->results[$name] = $error;
        }
    }

    public function run(): array
    {
        while (!$this->ready->isEmpty() || $this->waiting !== []) {
            $this->wakeWaiting();

            if ($this->ready->isEmpty()) {
                // 没有 ready 任务时,真实调度器应等待外部事件通知。
                continue;
            }

            [$name, $fiber] = $this->ready->dequeue();

            try {
                // 继续执行时,传入的值会成为 suspend() 的返回值。
                $signal = $fiber->resume($name . ' 已就绪');
                $this->accept($name, $fiber, $signal);
            } catch (Throwable $error) {
                // resume() 抛出的异常要进入统一结果收集路径。
                $this->results[$name] = $error;
            }
        }

        return $this->results;
    }

    private function accept(string $name, Fiber $fiber, mixed $signal): void
    {
        if ($fiber->isTerminated()) {
            // 只有终止后的 Fiber 才能读取 getReturn()。
            $this->results[$name] = $fiber->getReturn();
            return;
        }

        if ($signal instanceof WaitToken) {
            // 令牌把 Fiber 从 ready 区移入等待区。
            $this->waiting[$name] = ['fiber' => $fiber, 'ticks' => $signal->ticks];
            return;
        }

        throw new LogicException('任务返回了未知调度信号');
    }

    private function wakeWaiting(): void
    {
        foreach ($this->waiting as $name => $entry) {
            $entry['ticks']--;

            if ($entry['ticks'] ready->enqueue([$name, $entry['fiber']]);
                unset($this->waiting[$name]);
            } else {
                $this->waiting[$name] = $entry;
            }
        }
    }
}

$scheduler = new Scheduler();
$scheduler->add('订单', fn (): string => blockingTask('订单'));
$scheduler->add('库存', fn (): string => blockingTask('库存'));
$results = $scheduler->run();

这个实现刻意把“启动”“等待”“恢复”“完成”和“异常”放进同一条状态链:任务首次 start(),等待后进入 $waiting,就绪后回到 $ready,最后通过 getReturn() 收集结果。它是理解接口关系的骨架,不是已经具备网络事件复用能力的生产调度器。

把阻塞调用改造成可恢复任务

如果 Fiber 内部直接执行 sleep()、同步文件读取或阻塞式网络请求,PHP 线程会在这些调用返回前停住。此时调度器虽然保存了其他 Fiber,却没有机会运行它们。正确的工程拆分是:底层适配器先发起可等待操作,任务用 Fiber::suspend() 交出控制权,事件通知到达后由调度器调用 resume()。

PHP Fiber 任务、WaitToken、等待集合、ready 队列和结果异常回收关系说明图
图2:边界说明图,展示任务从等待信号回到恢复入口的关系,不是并发运行证据。

接入真实 I/O 时可以按下面的职责表设计:

组件应该负责不应该负责
Fiber 任务保持业务调用顺序,声明可暂停点自己遍历所有任务或猜测事件状态
调度器保存 Fiber、维护 ready/等待集合、传播异常把同步阻塞调用误当成非阻塞操作
等待适配器把 socket、定时器或客户端事件转为可恢复信号直接修改业务结果或吞掉错误
资源清理在 finally 中关闭句柄、释放锁和取消订阅依赖 Fiber 被销毁后才清理关键资源

异常、取消和资源清理不能留给调度循环猜

Fiber 可以通过 throw() 把异常注入暂停点,但调度器必须约定任务失败后的状态:是记录错误并继续其他任务,还是立即取消同一批任务。对于文件句柄、临时锁、事件订阅等资源,应在任务自己的 finally 中释放,不能只依赖调度器删除数组元素。

start();
    // 事件超时或取消时,把异常从暂停点交回任务。
    $fiber->throw(new RuntimeException('等待超时'));
} catch (Throwable $error) {
    // 这里记录任务错误,不能把异常伪装成成功结果。
    error_log($error->getMessage());
}

示例中的资源函数只是接口占位,没有声称可以直接运行。真实项目还应为取消、超时和重复恢复建立明确状态,避免对已终止 Fiber 再调用 resume(),也避免一个等待事件重复唤醒同一个任务。

什么时候值得把 Fiber 接入项目

如果项目已经有事件循环、异步客户端或定时器,只是希望业务代码保留同步风格,Fiber 很适合作为调度层和业务层之间的桥梁。若项目只有几个同步请求,直接引入自建调度器通常会增加状态管理、异常传播和取消语义的成本。

可以用下面的清单做决定:

  • 是否存在明确的可等待事件,而不是把 sleep() 放进 Fiber 伪装成异步?
  • 是否能保存每个 Fiber 的所有权,并保证 ready、等待和完成状态不会重复转换?
  • 是否有统一的超时、取消、异常和资源清理规则?
  • 是否能用已有异步库承担底层 I/O,而不是自己重写网络事件处理?

一句话总结:PHP Fiber 让阻塞风格的业务调用拥有暂停和恢复的入口,但“什么时候恢复、等待什么、失败怎么收尾”仍然要由调度器和 I/O 适配器共同完成。把这三层分开,才能把 Fiber 用成可维护的工程能力。

相关问题

PHP Fiber 会自动实现多线程吗?

不会。Fiber 是同一 PHP 执行上下文中的协作式暂停/恢复机制,不等于线程,也不会自动把 CPU 计算分发到多个核心。

Fiber 里可以直接调用阻塞式 HTTP 请求吗?

可以调用,但调用期间仍会阻塞当前线程;若希望其他任务继续运行,应使用能报告就绪事件的非阻塞客户端或异步适配器。

Fiber 和 Generator 应该怎么选?

Generator 更适合通过 yield 暴露迭代序列;Fiber 保存完整调用栈,适合在较深的调用链中暂停并由外部调度器恢复。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
PHP OPcache 怎样查看脚本缓存命中状态PHP OPcache 怎样查看脚本缓存命中状态
上一篇
PHP OPcache 怎样查看脚本缓存命中状态
PHP get_resource_id 怎样为文件句柄记录日志标识
下一篇
PHP get_resource_id 怎样为文件句柄记录日志标识
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之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模型性能。
    933次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    938次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1047次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1019次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    846次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码