基于imtoken下载框架部署详细教程(图)
PHP纤程(Fibers)在MCP SDK开发中的应用实践 本文通过PHPMCPSDK开发案例,深入解析了PHP纤程这一被低估的特性。纤程作为协作式多任务机制,能够优雅地解决复杂双向通信问题,同时保持API的简洁性。文章展示了纤程如何在不牺牲代码可读性的前提下,实现执行流的暂停和恢复,隐藏底层异步复杂性。 关键点包括: 纤程本质是协作式多任务,而非异步机制 通过挂起/恢复机制实现透明执行流控制 适
在 (MCP) SDK 开发过程中遇到的实际问题,深入探讨imtoken地址:imTke.app官网 PHP 纤程(Fibers)这一被低估的强大特性。文章详细展示了如何使用纤程解决复杂的双向通信问题,以及如何构建既优雅又实用的 API。

原文链接 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:让一切都异步
// 基于 Promise 的方法 - 嵌套且混乱
$server->addTool(function (string $dataset, $client) {
return $client->logAsync(LoggingLevel::Info, "开始分析")
->then(function() use ($client) {
return $client->progressAsync(0.33, 1, "步骤 1");
})
->then(function() use ($client) {
return $client->progressAsync(0.66, 1, "步骤 2");
})
->then(function() use ($client) {
return $client->sampleAsync("总结...");
})
->then(function($summary) {
return ['status' => 'complete', 'summary' => $summary];
});
});
回调嵌套很快就会变得笨拙。即使使用 await() 辅助函数来简化:
$server->addTool(function (string $dataset, $client) {
await($client->logAsync(LoggingLevel::Info, "开始"));
await($client->progressAsync(0.33, 1, "步骤 1"));
await($client->progressAsync(0.66, 1, "步骤 2"));
$summary = await($client->sampleAsync("总结..."));
return ['status' => 'complete', 'summary' => $summary];
});
这强制每个人学习异步 PHP。它使异步库成为核心依赖,并将 SDK 限制在选择的异步运行时。与服务器请求和响应的 PSR-7 不同,PHP 中没有事件循环或异步运行时的标准,因此供应商锁定不是一个选项。对于简单的工具来说,这也是过度杀伤。被拒绝。
选项 2:回调
// 回调地狱警告!
$server->addTool(function (string $dataset, $client) {
$client->log(..., function() use ($client) {
$client->progress(..., function() use ($client) {
$client->sample(..., function($summary) {
return ['summary' => $summary];
});
});
});
});
没人想要这个。无需进一步解释。被拒绝。
选项 3:状态机和序列化
如果我们跟踪执行状态并从检查点重新执行处理程序会怎么样?
// 伪代码
if ($state->step === 0) {
$client->log(...);
$state->step = 1;
return $state->serialize();
}
if ($state->step === 1) {
$client->progress(...);
$state->step = 2;
return $state->serialize();
}
// ...以此类推
这非常复杂。即使抽象部分内容并允许用户编写同步代码,跟踪状态也有很多工作要做。如何序列化闭包?如何恢复局部变量?如何处理循环?这将需要完全改变用户编写工具的方式。被拒绝。
选项 4:带 yield 的生成器
$server->addTool(function (string $dataset, $client) {
yield $client->log(...);
yield $client->progress(...);
$summary = yield $client->sample(...);
return ['summary' => $summary];
});
这更接近了,但生成器有限制。你不能轻松地从嵌套函数调用中 yield。语法很笨拙。用户需要理解生成器。不理想,但可行。
选项 5:PHP 纤程
如果挂起 / 恢复是不可见的会怎么样?如果 $client->log() 看起来像一个普通的方法调用,但在幕后它挂起纤程,发送消息,然后恢复呢?
// 用户编写的内容(看起来是同步的!)
$server->addTool(function (string $dataset, $client) {
$client->log(...); // 内部挂起
$client->progress(...); // 内部挂起
$summary = $client->sample(...); // 挂起并等待
return ['summary' => $summary];
});
就是这个。这就是解决方案。用户编写普通的 PHP。SDK 处理所有复杂性。
“啊哈!” 时刻
当开发团队意识到纤程是答案时,一切都豁然开朗了。以下是它们完美的原因:
透明:用户代码不需要知道纤程
灵活:适用于任何传输(阻塞或非阻塞)
简单:API 只是常规方法调用
强大:完全控制执行流
通用:适用于同步 PHP、异步 PHP、任何运行时
纤程让开发团队能够在干净的、看起来同步的 API 背后隐藏双向通信的复杂性。用户编写简单的函数。SDK 管理纤程生命周期。传输处理实际的 I/O。
这是完美的关注点分离。
思考过程:为什么纤程在这里有效
让我们深入了解为什么纤程特别适合解决这个问题。
核心挑战
当工具处理程序调用 $client->log() 时,需要:
暂停处理程序的执行
向客户端发送 JSON-RPC 通知(机制取决于传输)
立即恢复处理程序(日志记录不需要等待)
当工具处理程序调用 $client->sample() 时,需要:
暂停处理程序的执行
向客户端发送 JSON-RPC 请求(机制取决于传输)
等待客户端的响应(如何接收响应也取决于传输)
使用响应恢复处理程序
关键洞察:需要离开处理程序的执行上下文,做其他事情,然后在特定点返回。而且需要能够多次这样做。这正是纤程提供的功能。
架构
解决方案有三层:
ClientGateway(面向用户的 API)
提供 log()、progress()、sample() 等方法
内部调用 Fiber::suspend() 传递消息数据
恢复时返回响应
Protocol(编排层)
将处理程序执行包装在纤程中
检测纤程何时挂起
提取挂起的值(通知或请求)
将纤程移交给传输层
Transport(I/O 层)
获取挂起纤程的所有权
向客户端发送消息(响应、请求和通知)
等待响应(如果需要)
准备就绪时恢复纤程
每一层都有明确的职责。魔法在于它们如何协调。
为什么它同时适用于同步和异步
这种方法的美妙之处在于纤程与传输无关。无论你使用的是:
Stdio(阻塞,单进程)- 官方 SDK 的一部分
HTTP 与 PHP-FPM(无状态,多进程)- 官方 SDK 的一部分
ReactPHP(非阻塞,事件驱动)- 展示异步兼容性的外部示例
Swoole(基于协程)- 使用相同架构可行
挂起 / 恢复机制都是相同的。传输根据其执行模型决定何时恢复纤程。纤程本身不关心。它只是挂起和等待。
对于阻塞传输,恢复发生在同一进程的循环中。对于非阻塞传输,恢复通过事件循环回调发生。对于多进程传输,当从共享会话中提取响应时恢复发生。纤程不关心这些细节。
实现:架构概述
现在让我们深入实际实现。下面将展示一些来自 PHP MCP SDK 的真实代码,并解释一切如何组合在一起。
三个关键组件
系统有三个主要部分:
用户代码(处理程序)
↓
ClientGateway(API)
↓
Protocol(编排器)
↓
Transport(I/O)
移交:从 Protocol 到 Transport
这是关键时刻。以下是 Protocol 中的简化流程:
// src/Server/Protocol.php
// Protocol::handleRequest()
public function handleRequest(Request $request, SessionInterface $session): void {
$handler = $this->findHandler($request);
// 在纤程内执行处理程序!
$fiber = new \Fiber(fn() => $handler->handle($request, $session));
$result = $fiber->start();
if ($fiber->isSuspended()) {
// 纤程产生了某些东西!提取它。
if ($result['type'] === 'notification') {
$this->sendNotification($result['notification'], $session);
} elseif ($result['type'] === 'request') {
$this->sendRequest($result['request'], $result['timeout'], $session);
}
// 将纤程交给传输层
$this->transport->attachFiberToSession($fiber, $session->getId());
} else {
// 纤程完成而未挂起
$finalResult = $fiber->getReturn();
$this->sendResponse($finalResult, $session);
}
}
协议启动纤程并检查它是否挂起。如果挂起了,协议提取被挂起的内容(通知或请求),将其排队发送,并将纤程交给传输层。稍后会详细讨论这一点。现在让我们继续。
从这一点开始,传输层拥有纤程的生命周期。进一步的挂起和恢复由传输层处理。
Transport 的职责
每个传输必须:
从协议接受纤程
向客户端发送排队的消息
接收客户端响应
在适当的时间恢复纤程
处理纤程终止
不同的传输基于其执行模型以不同方式实现这一点。让我们看看每一个。
用户体验:它的外观
在深入传输实现之前,让我们看看从用户角度来看最终结果是什么样的。这很重要,因为它展示了为什么复杂性是值得的。
示例 1:简单的进度更新
这是来自 MCP SDK 文档的真实示例:
// server.php
$server->addTool(
function (string $dataset, ClientGateway $client): array {
$client->log(LoggingLevel::Info, "对数据集运行质量检查: $dataset");
$tasks = [
'验证 schema',
'扫描异常',
'审查统计摘要',
];
foreach ($tasks as $index => $task) {
$progress = ($index + 1) / count($tasks);
$client->progress($progress, 1, $task);
usleep(140_000); // 模拟工作
}
$client->log(LoggingLevel::Info, "数据集 $dataset 通过自动检查");
return [
'dataset' => $dataset,
'status' => 'passed',
'notes' => '未检测到重大问题',
];
},
name: 'run_dataset_quality_checks',
description: '执行带进度更新的数据集质量检查'
);
看看这个工具代码。它只是一个普通函数。它遍历任务。它调用 $client->progress(),就像调用普通方法一样。没有迹象表明这在进行复杂的双向通信。
但实际上发生的是:
处理程序启动(在纤程内)
$client->log() 挂起纤程
传输发送日志通知
纤程恢复
循环开始
第一个 $client->progress() 挂起纤程
传输发送进度通知
纤程恢复
usleep() 运行(仍在纤程中)
第二个 $client->progress() 再次挂起
… 以此类推
每次挂起和恢复对用户都是不可见的。代码看起来和表现得像同步 PHP。执行不断在传输的循环(现在是所有者)和工具的处理程序之间来回跳转,每次回到处理程序时,都回到完美的位置并恢复(甚至在 foreach 循环内)。请定义美!!
示例 2:请求 LLM 采样
这是一个更复杂的示例,实际等待响应:
// app/Tools/IncidentCoordinator.php
class IncidentCoordinator implements ClientAwareInterface {
use ClientAwareTrait; // 提供 $this->log 和 $this->progress
#[McpTool('coordinate_incident_response', '协调事件响应')]
public function coordinateIncident(string $incidentTitle): array {
$this->log(LoggingLevel::Warning, "事件分类开始: $incidentTitle");
$steps = [
'收集遥测数据',
'评估范围',
'协调响应者',
];
foreach ($steps as $index => $step) {
$progress = ($index + 1) / count($steps);
$this->progress($progress, 1, $step);
usleep(180_000);
}
// 请求客户端的 LLM 生成响应策略
$prompt = "为事件 \"$incidentTitle\" 提供简洁的响应策略
基于: " . implode(', ', $steps);
$result = $this->sample($prompt, 350, 90, ['temperature' => 0.5]);
$recommendation = $result->content instanceof TextContent
? trim($result->content->text)
: '';
$this->log(LoggingLevel::Info, "事件分类完成");
return [
'incident' => $incidentTitle,
'recommended_actions' => $recommendation,
'model' => $result->model,
];
}
}
这更加神奇。sample() 调用:
使用采样请求挂起纤程
传输向客户端发送请求
传输等待客户端响应(响应如何到来取决于传输,这可能需要几秒钟!)
当响应到达时,传输使用它恢复纤程
$result 包含响应,执行继续
从方法的角度来看,它进行了同步调用并得到了结果。在幕后:
纤程被挂起
控制权返回到传输的事件循环
传输处理其他事情(可能是其他请求)
当响应到来时(可能来自另一个 HTTP 请求 / 进程),纤程恢复
方法继续,就像什么都没发生过
这就是协作式多任务的实际应用。编写这个工具的开发者完全不知道这正在发生。
ClientAwareTrait 模式
注意上面示例中的 ClientAwareTrait。这是访问 ClientGateway 的两种方式之一:
方法 1:在处理程序中进行类型提示
#[McpTool('my_tool')]
public function myTool(string $input, ClientGateway $client): string {
$client->log(...);
return $result;
}
SDK 检测 ClientGateway 参数并自动注入它。
方法 2:实现 ClientAwareInterface
class MyService implements ClientAwareInterface {
use ClientAwareTrait; // 提供 setClient() 和辅助方法
#[McpTool('my_tool')]
public function myTool(string $input): string {
$this->log(...); // ClientAwareTrait 提供此方法
$this->progress(...);
$result = $this->sample(...);
return $result;
}
}
SDK 在调用处理程序之前调用 setClient(),trait 提供像 log()、progress()、sample() 这样的便捷方法,这些方法内部使用客户端。
两种方法都提供相同的能力。用户根据偏好选择。
底层:ClientGateway
现在让我们剥开第一层,看看 ClientGateway 如何工作。这是用户交互的 API。它出奇地(并不出奇地)简单,但超级强大。
这是实际实现(为清晰起见进行了简化):
// src/Server/ClientGateway.php
final class ClientGateway {
public function __construct(
private readonly SessionInterface $session,
) {}
/**
* 向客户端发送通知(即发即忘)。
*/
public function notify(Notification $notification): void {
\Fiber::suspend([
'type' => 'notification',
'notification' => $notification,
'session_id' => $this->session->getId()->toRfc4122(),
]);
}
/**
* 向客户端发送日志消息。
*/
public function log(LoggingLevel $level, mixed $data, ?string $logger = null): void {
$this->notify(new LoggingMessageNotification($level, $data, $logger));
}
/**
* 向客户端发送进度更新。
*/
public function progress(float $progress, ?float $total = null, ?string $message = null): void {
$meta = $this->session->get(Protocol::SESSION_ACTIVE_REQUEST_META, []);
$progressToken = $meta['progressToken'] ?? null;
if (null === $progressToken) {
// 客户端未请求进度,跳过
return;
}
$this->notify(new ProgressNotification($progressToken, $progress, $total, $message));
}
/**
* 从客户端请求 LLM 采样。
*/
public function sample(
array|Content|string $message,
int $maxTokens = 1000,
int $timeout = 120,
array $options = []
): CreateSamplingMessageResult {
// 准备消息
if (is_string($message)) {
$message = new TextContent($message);
}
if ($message instanceof Content) {
$message = [new SamplingMessage(Role::User, $message)];
}
$request = new CreateSamplingMessageRequest(
messages: $message,
maxTokens: $maxTokens,
preferences: $options['preferences'] ?? null,
systemPrompt: $options['systemPrompt'] ?? null,
temperature: $options['temperature'] ?? null,
// ...其他选项
);
// 发送请求并等待响应
$response = $this->request($request, $timeout);
if ($response instanceof Error) {
throw new ClientException($response);
}
return CreateSamplingMessageResult::fromArray($response->result);
}
/**
* 向客户端发送请求并等待响应。
*/
private function request(Request $request, int $timeout = 120): Response|Error {
$response = \Fiber::suspend([
'type' => 'request',
'request' => $request,
'session_id' => $this->session->getId()->toRfc4122(),
'timeout' => $timeout,
]);
if (!$response instanceof Response && !$response instanceof Error) {
throw new RuntimeException('传输返回了意外的载荷');
}
return $response;
}
}
关键方法
notify() - 最简单的情况:
public function notify(Notification $notification): void {
\Fiber::suspend([
'type' => 'notification',
'notification' => $notification,
'session_id' => $this->session->getId()->toRfc4122(),
]);
}
这会使用一个数据结构挂起当前纤程,该结构指示:
这是一个通知(不需要响应)
要发送什么通知
它属于哪个会话
纤程将在通知排队后立即恢复。
request() - 复杂的情况:
private function request(Request $request, int $timeout = 120): Response|Error {
$response = \Fiber::suspend([
'type' => 'request',
'request' => $request,
'session_id' => $this->session->getId()->toRfc4122(),
'timeout' => $timeout,
]);
return $response;
}
这会使用一个数据结构挂起纤程,该结构指示:
这是一个请求(期望响应)
要发送什么请求
等待响应多长时间
纤程在以下情况之前不会恢复:
客户端发送响应,或
超时到期
当恢复时,传递给 $fiber->resume($value) 的值成为 Fiber::suspend() 的返回值。因此 $response 将是 Response 对象(成功)或 Error 对象(失败 / 超时)。
何时应该使用纤程?
现在你已经看到了纤程的实际应用,什么时候应该在自己的代码中真正使用它们?
纤程使用案例检查清单
在以下情况考虑使用纤程:
✅ 需要暂停和恢复执行 - 核心使用案例。如果你需要离开一个函数,做其他事情,然后回来。
✅ 想要对用户隐藏复杂性 - 如果你正在构建一个库,并希望提供一个干净的 API 来隐藏异步或有状态的行为。
✅ 需要协作式多任务 - 当你希望多个” 任务” 在没有线程或进程的情况下取得进展。
✅ 正在桥接同步和异步代码 - 当你想让异步操作看起来同步时(如 ReactPHP 的 await)。
✅ 需要维护执行上下文 - 当使用生成器暂停和恢复会受到太多限制时(不能轻松地从嵌套调用中 yield)。
✅ 正在构建基础设施代码 - 库、框架和 SDK 最能从纤程中受益。
何时不使用纤程
在以下情况不要使用纤程:
❌ 简单的回调就足够了 - 不要把事情复杂化。如果回调有效,就使用回调。
❌ 需要真正的并行性 - 纤程是协作的,不是并行的。使用进程、线程或异步 I/O 实现并行性。
❌ 代码简单且线性 - 如果不需要中断或恢复,纤程会增加不必要的复杂性。
❌ 你不控制执行流 - 纤程在库和框架中表现出色,在应用代码中则较少。
❌ 生成器工作正常 - 如果生成器(yield)干净地解决了你的问题,坚持使用它们。纤程更强大但也更复杂。
常见陷阱和注意事项
1. 理解谁控制纤程
关于纤程最基本的理解:当纤程挂起时,它将控制权让回给某人。那个” 某人” 就是编排器,你需要知道它是谁。
纤程代表一个工作单元。当它调用 Fiber::suspend() 时,执行跳出纤程并返回到调用 $fiber->start() 或 $fiber->resume() 的实体。该实体负责决定何时(以及是否)恢复纤程。
在 MCP 传输中:
StdioTransport:主循环(while (!feof($input)))是编排器。它持续处理输入、管理纤程并刷新输出。
StreamableHttpTransport:SSE 流的阻塞循环在该请求的生命周期内成为编排器。它阻塞整个进程并管理纤程直到完成。
ReactPHP:事件循环是编排器。我们不阻塞它;相反,我们注册循环管理的定时器。
关键原则:编排器不能被永久阻塞,否则你的纤程永远不会恢复。如果你正在编写挂起纤程的代码,确保接收控制权的实体有机制来恢复它们。
还要注意:纤程可以嵌套。你可以在纤程内部创建纤程。编排器可以是一个中央管理器(如我们的 TaskManager 示例所示),或者父纤程本身可以充当其子纤程的编排器。只需清楚谁在管理谁,并确保编排器不会无限期地被阻塞。
2. 忘记你在纤程中
function myHandler() {
$client->sample("生成文本"); // 这会挂起!
// 这里的任何代码都在挂起和恢复之后运行
}
记住挂起可能发生在调用栈的深处。始终考虑在挂起期间可能改变的状态。
3. 资源生命周期
$lock = $mutex->acquire();
$client->sample("..."); // 纤程在这里挂起
$lock->release(); // 这会晚得多才运行!
小心跨越挂起点的资源(锁、数据库事务、文件句柄)。纤程可能会挂起几秒钟或几分钟。
4. 跨挂起的异常处理
try {
$result = $client->sample("..."); // 挂起
} catch (\Throwable $e) {
// 这捕获纤程内部的异常
// 不是挂起期间的异常
// 除非纤程用 throw() 恢复
}
异常在纤程内正常工作,但挂起 / 恢复机制本身有单独的错误处理。因此理解异常不会自动跨越纤程和编排器之间的边界至关重要。
如果纤程抛出异常,它会冒泡到编排器(通过 $fiber->start() 或 $fiber->resume())。如果编排器抛出异常,它不会自动进入纤程(因为异常可能与挂起的纤程相关,也可能无关)。
你必须明确决定如何桥接这个差距。你是希望编排器独立崩溃吗?你想捕获错误并使用失败对象 resume() 吗?还是你想将其 throw() 到纤程中?这些是架构决策,不是默认行为。
5. 全局状态
global $counter;
$counter++;
$client->log("计数:$counter"); // 挂起
$counter++; // 如果另一个纤程在挂起期间修改了 $counter 会怎样?
小心全局状态。其他代码(或其他纤程)可能在你挂起时修改它。
6. 纤程创建开销
创建纤程有少量开销。不要创建数百万个。与线程相比它们是轻量级的,但不是免费的。
结论:理解你的工具的力量
当你学习数据结构和算法时,你不仅仅是记忆定义和语法,你还学习何时使用它们。例如,如果你不认识何时需要在两端快速插入 / 删除,双向链表就没有用。
这同样适用于语言特性。PHP 自 8.1 以来就有了纤程,但大多数开发者不使用它们,因为他们不认识纤程解决的问题。所以,下次当你面临涉及以下问题时:
暂停和恢复执行
在干净的 API 背后隐藏复杂性
使异步代码感觉同步
协作式多任务
问自己:” 纤程能优雅地解决这个问题吗?”
你可能会惊讶于答案是肯定的频率。
总结
PHP 纤程(Fibers)自 PHP 8.1 引入以来,一直是一个被低估的特性。通过 PHP MCP SDK 的客户端通信功能(PR #109)这个实际案例,我们看到了纤程如何优雅地解决复杂的架构问题。
这个实现的精妙之处不在于其技术复杂度,而在于其设计理念 —— 通过纤程将复杂的双向通信机制隐藏在简洁的同步风格 API 背后,让用户能够编写直观、易读的代码,而无需关心底层的挂起、恢复和状态管理。
关键要点
纤程不是异步机制,而是协作式多任务的实现,是管理执行流的工具
正确的抽象层次:让复杂性在库层面解决一次,所有用户受益
传输无关性:同一套纤程机制可以适配不同的 I/O 模型(阻塞、非阻塞、多进程)
用户友好:最好的技术是让问题对用户消失的技术
适用场景
纤程最适合:
构建库和框架
隐藏异步复杂性
需要暂停和恢复执行上下文的场景
桥接同步和异步代码
不适用于:
简单的线性流程
需要真正并行性的场景
可以用简单回调或生成器解决的问题
选择正确的工具,理解工具的本质,是构建优雅软件的关键。PHP 纤程正是这样一个被低估但极其强大的工具。
更多推荐


所有评论(0)