登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  php教程

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

来源:17golang原创

时间:2026-11-08 00:38:05 474浏览 收藏

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删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>