news 2026/4/16 13:55:59

PHP Fiber 优雅协作式多任务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP Fiber 优雅协作式多任务

在开发官方 PHP MCP SDK 的客户端通信功能时,开发团队遇到了一个看似无法优雅解决的架构挑战。传统的异步方案、回调模式和状态机都无法在不牺牲代码简洁性的前提下实现需求。最终,PHP 纤程(Fibers)成为了这个问题的完美解决方案。

该功能在 PR #109 中引入,其实现展示了 PHP 纤程最优雅的使用案例之一。但这不仅仅是关于一个问题的故事,更是关于一个自 PHP 8.1 以来一直隐藏在众目睽睽之下、却被大量误解和未充分利用的强大 PHP 特性。

本文将深入探讨:

PHP 纤程到底是什么(以及它们不是什么)

何时以及为何应该使用它们

如何理解协作式多任务

使用纤程的真实世界实现

可以在自己代码中使用的模式

文章较长,建议准备好咖啡慢慢看。

关于纤程的误解

首先要解决房间里的大象:PHP 纤程不是异步 PHP。它们不是并行机制,不是线程,也不是让 PHP 同时运行多个任务。

当 PHP 8.1 在 2021 年 11 月引入纤程支持时,许多开发者都感到困惑。"太好了,又一个异步东西?"大家这样想。这种困惑是可以理解的,因为纤程最显眼的使用场景一直是在 ReactPHP 和 AmPHP 等异步库中。

ReactPHP 甚至有一个名为 async 的包,使用纤程让异步代码看起来像同步代码:

// 纤程之前:回调地狱

$promise->then(function($result) {

return anotherAsyncCall($result);

})->then(function($finalResult) {

echo $finalResult;

});

// 使用纤程:看起来是同步的!

$result = await($promise);

$finalResult = await(anotherAsyncCall($result));

echo $finalResult;

看到这个,很容易认为"纤程 = 异步魔法"。但这忽略了更大的图景。

纤程的本质是协作式多任务。它们赋予代码暂停执行、执行其他操作、然后在完全保留所有变量、调用栈和执行上下文的情况下,精确地回到离开的位置继续执行的能力。

是的,这对异步库非常有用。但在需要受控中断和恢复的纯同步代码中,它同样有用。而这正是大多数 PHP 开发者错过的机会。

纤程采用缓慢的原因不是因为它们不够有用,而是因为大多数开发者不知道何时使用它们。而这正是本文要解决的问题。

理解纤程:基础知识

在深入复杂示例之前,让我们先建立坚实的基础。纤程到底是什么,它如何工作?

什么是协作式多任务?

理解纤程的一个好类比是,将标准 PHP 脚本想象成单轨道上的火车。它从 A 站开到 B 站,通常在到达 B 之前不能停止。纤程允许火车在轨道中间停下来,让乘客下车(或让乘客上洗手间休息),在此期间甚至让另一列火车使用这条轨道,然后在所有行李(变量和内存状态)完好无损的情况下,精确地从停下的地方恢复。

另一个类比是想象你在做饭的同时读书。你读几页书,然后定时器响了,你标记页面,搅拌锅里的东西,再回到刚才读到的地方继续阅读。这就是协作式多任务。

关键词是协作。你(读者/厨师)决定何时切换任务。没有人强行打断你,而是在合适的时候自愿交出控制权。

在编程术语中:

抢占式多任务:操作系统强制中断你的代码(线程、进程)

协作式多任务:你的代码决定何时交出控制权(协程、纤程)

纤程是 PHP 对协作式多任务的实现。它们让你能够:

开始执行一段代码

在任何点暂停它(挂起)

做其他事情

精确地从离开的地方恢复

根据需要重复任意多次

纤程的结构

让我们看一个简单的例子:

<?php

$fiber = new Fiber(function(): string {

echo "1. 纤程启动\n";

$value = Fiber::suspend('pause-1');

echo "3. 纤程恢复,收到: $value\n";

$value2 = Fiber::suspend('pause-2');

echo "5. 纤程再次恢复,收到: $value2\n";

return 'final-result';

});

echo "0. 启动纤程之前\n";

$suspended1 = $fiber->start();

echo "2. 纤程挂起,返回: $suspended1\n";

$suspended2 = $fiber->resume('data-1');

echo "4. 纤程再次挂起,返回: $suspended2\n";

$result = $fiber->resume('data-2');

echo "6. 纤程返回: $result\n";

输出:

0. 启动纤程之前

1. 纤程启动

2. 纤程挂起,返回: pause-1

3. 纤程恢复,收到: data-1

4. 纤程再次挂起,返回: pause-2

5. 纤程再次恢复,收到: data-2

6. 纤程返回: final-result

这里特意包含了数字,以便读者看清执行如何在纤程内外跳转。suspend 让它跳出纤程,resume 让它跳回纤程!为了更清晰,让我们分解一下发生了什么:

创建:new Fiber(function() {...}) 创建纤程但尚未执行

启动:$fiber->start() 开始执行,直到第一个 Fiber::suspend()

挂起:Fiber::suspend('pause-1') 暂停执行并将控制权返回给调用者

恢复:$fiber->resume('data-1') 从挂起处继续执行

返回:当纤程完成时,resume() 返回最终值

魔法在于执行上下文切换。当纤程挂起时:

所有局部变量都被保留

调用栈被保存

执行跳回到调用 start() 或 resume() 的地方

传递给 suspend() 的值返回给调用者

当你恢复时:

执行跳回纤程内部

传递给 resume() 的值成为 suspend() 的返回值

一切继续,就像什么都没发生过

一个让纤程变得强大的关键洞察:在纤程内部运行的代码不需要知道它在纤程中。

看看这个:

function processData(int $id): string {

$data = fetchData($id); // 这可能会挂起!

$result = transform($data); // 这也可能会挂起!

return $result;

}

// 在纤程内调用

$fiber = new Fiber(fn() => processData(42));

$fiber->start();

从 processData 的角度来看,它只是在调用函数并返回结果。它不知道 fetchData() 和 transform() 可能在幕后挂起纤程。复杂性是隐藏的。

这正是纤程非常适合构建隐藏复杂行为的干净 API 的原因。

异步库中的纤程

现在我们理解了基础知识,让我们看看为什么有些人会将纤程与异步代码联系起来。这也会在我们处理主要问题之前展示一个具体的使用案例。

异步问题

PHP 中的传统异步编程看起来像这样:

// 使用 promises(纤程之前)

function fetchUserData(int $userId): PromiseInterface {

return $this->httpClient->getAsync("/users/$userId")

->then(function($response) {

return json_decode($response->getBody());

})

->then(function($userData) use ($userId) {

return $this->cache->setAsync("user:$userId", $userData);

})

->then(function() use ($userId) {

return "User $userId cached";

});

}

这能工作,但很难阅读和理解。使用 catch() 的错误处理会变得混乱。调试很痛苦。而且感觉不像 PHP。

纤程解决方案

有了纤程,像 ReactPHP 这样的库可以提供这样的方式:

// 使用纤程(PHP 8.1 之后)

function fetchUserData(int $userId): string {

$response = await($this->httpClient->getAsync("/users/$userId"));

$userData = json_decode($response->getBody());

await($this->cache->setAsync("user:$userId", $userData));

return "User $userId cached";

}

好多了!但 await() 是如何工作的呢?让我们看一个简化版本:

namespace React\Async;

function await(PromiseInterface $promise): mixed {

// 挂起纤程并注册 promise 回调

$result = Fiber::suspend([

'type' => 'await',

'promise' => $promise

]);

// 恢复时,我们将得到结果或异常

if ($result instanceof \Throwable) {

throw $result;

}

return $result;

}

如果你感兴趣,像 PHPStan 这样的工具可以让你添加一些泛型魔法,这样 await() 就能准确知道从你的 Promise 返回什么。这种强大的静态分析感觉就像魔法。多酷啊?

以下是发生的过程:

用户代码调用 await($promise)(在纤程内部)

await() 调用 Fiber::suspend() 传递 promise

事件循环看到挂起的纤程和 promise

事件循环在纤程挂起时照常继续处理其他事情

当 promise 解决时,循环调用 $fiber->resume($value)

执行在 await() 中继续,返回值

用户代码得到值,就像它是同步的!

纤程在等待异步操作时挂起,但用户的代码看起来完全是同步的。

更进一步:真正透明的异步

但我们可以走得更远!像 AmPHP 这样的库通过创建围绕异步操作的纤程感知包装器,将其提升到新的水平。你不需要单独的 getAsync() 和 await() 调用,只需要看起来完全同步的方法:

// AmPHP 方法:不需要 await()!

function fetchUserData(int $userId): string {

$response = $this->httpClient->get("/users/$userId"); // 看起来同步,实际异步!

$userData = json_decode($response->getBody());

$this->cache->set("user:$userId", $userData); // 看起来同步,实际异步!

return "User $userId cached";

}

等等,什么?没有 await() 调用?这是如何工作的?

魔法在于 get() 和 set() 内部使用纤程。这是一个简化的例子:

class HttpClient {

public function get(string $url): Response {

// 创建异步操作

$promise = $this->performAsyncRequest('GET', $url);

// 挂起当前纤程并将 promise 传递给事件循环

$response = \Fiber::suspend([

'type' => 'await',

'promise' => $promise

]);

if ($response instanceof \Throwable) {

throw $response;

}

return $response;

}

}

从用户的角度来看,他们只是调用了 get() 并得到了响应。他们完全不知道这是异步的。

这就是纤程的精髓:让异步操作完全透明。用户编写看起来像阻塞的同步 PHP 代码。库使用纤程在幕后处理所有异步复杂性。

比较这些方法

让我们看看演变过程:

// 1. 传统异步与 promises(无纤程)

$promise = $this->httpClient->getAsync("/users/$userId")

->then(fn($response) => json_decode($response->getBody()))

->then(fn($userData) => $this->cache->setAsync("user:$userId", $userData))

->then(fn() => "User $userId cached");

// 2. 使用 await() 辅助函数的异步(使用纤程)

$response = await($this->httpClient->getAsync("/users/$userId"));

$userData = json_decode($response->getBody());

await($this->cache->setAsync("user:$userId", $userData));

return "User $userId cached";

// 3. 完全透明的异步(纤程隐藏在库中)

$response = $this->httpClient->get("/users/$userId");

$userData = json_decode($response->getBody());

$this->cache->set("user:$userId", $userData);

return "User $userId cached";

注意方法 #3 看起来与同步代码完全一样?这就是正确使用纤程的力量。库开发者处理一次复杂性。每个用户都受益于一个干净的、看起来同步的 API,实际上在底层是异步的。

为什么这导致了误解

因为纤程最显眼的用途是让异步代码看起来同步,开发者假设纤程本身就是异步机制。但纤程本身不做任何异步操作。它们只是提供挂起/恢复机制,使得看起来同步的异步代码成为可能。

事件循环仍在做实际的异步工作。纤程只是让 API 更好用。

这个区别至关重要:纤程是管理执行流的工具,而不是实现并行或异步的工具。

真正的问题:MCP SDK 中的客户端通信

现在让我们进入本文的核心问题。在开发 Model Context Protocol (MCP) 的 PHP 实现时,开发团队遇到了一个似乎无法优雅解决的设计挑战。

什么是 MCP?

Model Context Protocol 是连接 AI 助手(如 Claude)与外部工具和数据源的标准。

一个 MCP 服务器暴露:

工具:AI 可以调用的函数(例如:"搜索数据库"、"发送邮件")

资源:AI 可以读取的数据(例如:"项目文件"、"API 文档")

提示:AI 可以使用的模板

该协议是双向的 JSON-RPC,支持不同的传输方式(STDIO、HTTP + SSE、自定义)。

挑战

MCP 规范包含服务器在请求处理期间与客户端通信的功能:

日志记录:向客户端发送日志消息

进度更新:更新客户端关于长时间运行操作的进度

采样:请求客户端使用其 LLM 生成文本

这些不仅仅是响应类型。不,问题是它们需要在工具执行期间发生。例如:

客户端: "嘿服务器,运行 'analyze_dataset' 工具"

服务器: "开始..." [发送日志]

服务器: "25% 完成" [发送进度]

服务器: "50% 完成" [发送进度]

服务器: "生成摘要,需要你的 LLM" [发送采样请求]

客户端: "这是生成的摘要" [响应采样]

服务器: "完成!这是完整结果" [发送最终响应]

服务器需要:

在执行过程中发送消息

等待来自客户端的响应

在收到响应后继续执行

让所有这些感觉起来很自然

API 需求

在 MCP SDK 方面,优先事项之一是使其极其易用。开发团队希望开发者这样编写工具:

$server->addTool(

function (string $dataset, ClientGateway $client): array {

$client->log(LoggingLevel::Info, "开始分析");

foreach ($steps as $step) {

$client->progress($progress, 1, $step);

doWork($step);

}

$summary = $client->sample("总结这些数据:...");

return ['status' => 'complete', 'summary' => $summary];

},

name: 'analyze_dataset'

);

看看这段代码。它很漂亮。它很简单。它看起来完全是同步的。没有回调,没有 promises,没有 async/await 语法,没有 yield 生成器。只是普通的 PHP。

但在底层,这需要:

向客户端发送 JSON-RPC 通知(日志、进度)

发送 JSON-RPC 请求并等待响应(采样)

与任何传输方式工作,无论是否阻塞!

无论你使用原生 PHP、ReactPHP、Swoole 还是 RoadRunner 都能工作

如何实现?

为什么传统方法行不通

开发团队花了几个小时考虑不同的解决方案:

选项 1:让一切都异步

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/13 23:31:12

CDH大数据平台入门:从零开始搭建第一个集群

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 请创建一个适合新手的CDH平台入门教程&#xff0c;包含&#xff1a;1. 最小化硬件需求说明 2. 单节点伪集群安装步骤 3. HDFS基础操作示例 4. YARN作业提交演示 5. 常见问题排查指南…

作者头像 李华
网站建设 2026/4/11 20:41:41

基于‘CEEMDAN-VMD-TCN-BiGRU‘组合方法的短期电力负荷时间序列预测

基于 CEEMDAN-VMD-TCN-BiGRU 的短期电力负荷时间序列预测 python代码 代码 CEEMDAN-VMD-TCN-BiGRU组合预测方法&#xff1a; 1 采用CEEMDAN将原始电力负荷数据分解成一组比较稳定的子序列&#xff0c;联合 小波阈值法将含有噪声的高频分量去噪&#xff0c;保留含有信号的低频…

作者头像 李华
网站建设 2026/4/15 21:23:03

30、文件事件监控与内存管理:原理、操作及优化策略

文件事件监控与内存管理:原理、操作及优化策略 1. 文件事件监控 1.1 添加新监控 在现有的 inotify 实例中添加新的监控可以按以下方式操作: int wd; wd = inotify_add_watch (fd, "/etc", IN_ACCESS | IN_MODIFY); if (wd == -1) {perror ("inotify_add_…

作者头像 李华
网站建设 2026/4/15 9:36:48

38、Unix时间处理函数全解析

Unix时间处理函数全解析 1. 获取当前时间 1.1 time()函数 time() 函数用于返回自纪元(epoch)以来经过的秒数来表示当前时间。如果参数 t 不为 NULL ,该函数还会将当前时间写入提供的指针。 #include <time.h> time_t t; printf ("current time: %ld\n&…

作者头像 李华
网站建设 2026/3/28 6:44:20

39、Linux 时间处理与定时器机制详解

Linux 时间处理与定时器机制详解 1. 纳秒级睡眠 在 Linux 系统中, usleep() 函数已被弃用,取而代之的是 nanosleep() 函数,它提供了纳秒级的分辨率和更智能的接口。 #define _POSIX_C_SOURCE 199309 #include <time.h> int nanosleep (const struct timespec …

作者头像 李华
网站建设 2026/4/15 16:42:24

LLM应用剖析: 热点新闻助手TrendRadar

1. 背景花了近三天时间&#xff0c;深入研究了Github近几天一直霸榜的热门项目TrendRadar&#xff0c;星标已达30K&#xff0c;与先前的开源项目微舆&#xff0c;成为11月份github的趋势榜国产双雄。项目的部署小白直接入手&#xff0c;基于Github Action实现一键配置与部署&am…

作者头像 李华