PHP Fiber 優雅協作式多任務 在 PHP Model Context Protocol (MCP) SDK 開發過程中遇到的實際問題,深入探討了 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 的包,使用纖程讓異步代碼看起來像同步代碼:

// 纖程之前:回調地獄 PHP Fiber 優雅協作式多任務_客户端result) { return anotherAsyncCall(PHP Fiber 優雅協作式多任務_PHP_02finalResult) { echo $finalResult; });

// 使用纖程:看起來是同步的! PHP Fiber 優雅協作式多任務_處理程序_03promise); PHP Fiber 優雅協作式多任務_客户端_04result)); 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"; 輸出:

  1. 啓動纖程之前
  2. 纖程啓動
  3. 纖程掛起,返回: pause-1
  4. 纖程恢復,收到: data-1
  5. 纖程再次掛起,返回: pause-2
  6. 纖程再次恢復,收到: data-2
  7. 纖程返回: final-result 這裏特意包含了數字,以便讀者看清執行如何在纖程內外跳轉。suspend 讓它跳出纖程,resume 讓它跳回纖程!為了更清晰,讓我們分解一下發生了什麼:

創建:new Fiber(function() {...}) 創建纖程但尚未執行 啓動:PHP Fiber 優雅協作式多任務_客户端_05fiber->resume('data-1') 從掛起處繼續執行 返回:當纖程完成時,resume() 返回最終值 魔法在於執行上下文切換。當纖程掛起時:

所有局部變量都被保留 調用棧被保存 執行跳回到調用 start() 或 resume() 的地方 傳遞給 suspend() 的值返回給調用者 當你恢復時:

執行跳回纖程內部 傳遞給 resume() 的值成為 suspend() 的返回值 一切繼續,就像什麼都沒發生過 一個讓纖程變得強大的關鍵洞察:在纖程內部運行的代碼不需要知道它在纖程中。

看看這個:

function processData(int $id): string { PHP Fiber 優雅協作式多任務_PHP_06id); // 這可能會掛起! PHP Fiber 優雅協作式多任務_客户端_07data); // 這也可能會掛起! return $result; }

// 在纖程內調用 PHP Fiber 優雅協作式多任務_處理程序_08fiber->start(); 從 processData 的角度來看,它只是在調用函數並返回結果。它不知道 fetchData() 和 transform() 可能在幕後掛起纖程。複雜性是隱藏的。

這正是纖程非常適合構建隱藏複雜行為的乾淨 API 的原因。

異步庫中的纖程 現在我們理解了基礎知識,讓我們看看為什麼有些人會將纖程與異步代碼聯繫起來。這也會在我們處理主要問題之前展示一個具體的使用案例。

異步問題 PHP 中的傳統異步編程看起來像這樣:

// 使用 promises(纖程之前) function fetchUserData(int $userId): PromiseInterface { return PHP Fiber 優雅協作式多任務_PHP_09userId") ->then(function(PHP Fiber 優雅協作式多任務_客户端_10response->getBody()); }) ->then(function(PHP Fiber 優雅協作式多任務_PHP_11userId) { return PHP Fiber 優雅協作式多任務_處理程序_12userId", PHP Fiber 優雅協作式多任務_PHP_13userId) { return "User $userId cached"; }); } 這能工作,但很難閲讀和理解。使用 catch() 的錯誤處理會變得混亂。調試很痛苦。而且感覺不像 PHP。

纖程解決方案 有了纖程,像 ReactPHP 這樣的庫可以提供這樣的方式:

// 使用纖程(PHP 8.1 之後) function fetchUserData(int $userId): string { PHP Fiber 優雅協作式多任務_客户端_14this->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 解決時,循環調用 PHP Fiber 優雅協作式多任務_客户端_15value) 執行在 await() 中繼續,返回值 用户代碼得到值,就像它是同步的! 纖程在等待異步操作時掛起,但用户的代碼看起來完全是同步的。

更進一步:真正透明的異步 但我們可以走得更遠!像 AmPHP 這樣的庫通過創建圍繞異步操作的纖程感知包裝器,將其提升到新的水平。你不需要單獨的 getAsync() 和 await() 調用,只需要看起來完全同步的方法:

// AmPHP 方法:不需要 await()! function fetchUserData(int $userId): string { $response = PHP Fiber 優雅協作式多任務_客户端_16userId"); // 看起來同步,實際異步!

$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 = PHP Fiber 優雅協作式多任務_PHP_09userId") ->then(fn(PHP Fiber 優雅協作式多任務_PHP_18response->getBody())) ->then(fn($userData) => PHP Fiber 優雅協作式多任務_處理程序_12userId", $userData)) ->then(fn() => "User $userId cached");

// 2. 使用 await() 輔助函數的異步(使用纖程) PHP Fiber 優雅協作式多任務_客户端_14this->httpClient->getAsync("/users/PHP Fiber 優雅協作式多任務_客户端_21userData = json_decode(PHP Fiber 優雅協作式多任務_客户端_22this->cache->setAsync("user:$userId", $userData)); return "User $userId cached";

// 3. 完全透明的異步(纖程隱藏在庫中) $response = PHP Fiber 優雅協作式多任務_客户端_16userId"); PHP Fiber 優雅協作式多任務_處理程序_24response->getBody()); PHP Fiber 優雅協作式多任務_處理程序_25userId", $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 PHP Fiber 優雅協作式多任務_客户端_26client) { return PHP Fiber 優雅協作式多任務_處理程序_27client) { return PHP Fiber 優雅協作式多任務_處理程序_28client) { return PHP Fiber 優雅協作式多任務_客户端_29summary) { return ['status' => 'complete', 'summary' => $summary]; }); }); 回調嵌套很快就會變得笨拙。即使使用 await() 輔助函數來簡化:

$server->addTool(function (string $dataset, PHP Fiber 優雅協作式多任務_處理程序_30client->logAsync(LoggingLevel::Info, "開始")); await(PHP Fiber 優雅協作式多任務_PHP_31client->progressAsync(0.66, 1, "步驟 2")); PHP Fiber 優雅協作式多任務_客户端_32client->sampleAsync("總結...")); return ['status' => 'complete', 'summary' => $summary]; }); 這強制每個人學習異步 PHP。它使異步庫成為核心依賴,並將 SDK 限制在選擇的異步運行時。與服務器請求和響應的 PSR-7 不同,PHP 中沒有事件循環或異步運行時的標準,因此供應商鎖定不是一個選項。對於簡單的工具來説,這也是過度殺傷。被拒絕。

選項 2:回調

// 回調地獄警告! $server->addTool(function (string $dataset, $client) { PHP Fiber 優雅協作式多任務_客户端_33client) { PHP Fiber 優雅協作式多任務_PHP_34client) { PHP Fiber 優雅協作式多任務_客户端_35summary) { return ['summary' => $summary]; }); }); }); }); 沒人想要這個。無需進一步解釋。被拒絕。

選項 3:狀態機和序列化

如果我們跟蹤執行狀態並從檢查點重新執行處理程序會怎麼樣?

// 偽代碼 if ($state->step === 0) { $client->log(...); $state->step = 1; return PHP Fiber 優雅協作式多任務_處理程序_36state->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 = PHP Fiber 優雅協作式多任務_客户端_37request);

// 在纖程內執行處理程序!
$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;

} 這會使用一個數據結構掛起纖程,該結構指示:

這是一個請求(期望響應) 要發送什麼請求 等待響應多長時間 纖程在以下情況之前不會恢復:

客户端發送響應,或 超時到期 當恢復時,傳遞給 PHP Fiber 優雅協作式多任務_客户端_15value) 的值成為 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 示例所示),或者父纖程本身可以充當其子纖程的編排器。只需清楚誰在管理誰,並確保編排器不會無限期地被阻塞。

  1. 忘記你在纖程中 function myHandler() { $client->sample("生成文本"); // 這會掛起! // 這裏的任何代碼都在掛起和恢復之後運行 } 記住掛起可能發生在調用棧的深處。始終考慮在掛起期間可能改變的狀態。
  2. 資源生命週期 $lock = PHP Fiber 優雅協作式多任務_客户端_39client->sample("..."); // 纖程在這裏掛起 $lock->release(); // 這會晚得多才運行! 小心跨越掛起點的資源(鎖、數據庫事務、文件句柄)。纖程可能會掛起幾秒鐘或幾分鐘。
  3. 跨掛起的異常處理 try { $result = $client->sample("..."); // 掛起 } catch (\Throwable $e) { // 這捕獲纖程內部的異常 // 不是掛起期間的異常 // 除非纖程用 throw() 恢復 } 異常在纖程內正常工作,但掛起 / 恢復機制本身有單獨的錯誤處理。因此理解異常不會自動跨越纖程和編排器之間的邊界至關重要。

如果纖程拋出異常,它會冒泡到編排器(通過 $fiber->start() 或 $fiber->resume())。如果編排器拋出異常,它不會自動進入纖程(因為異常可能與掛起的纖程相關,也可能無關)。

你必須明確決定如何橋接這個差距。你是希望編排器獨立崩潰嗎?你想捕獲錯誤並使用失敗對象 resume() 嗎?還是你想將其 throw() 到纖程中?這些是架構決策,不是默認行為。

  1. 全局狀態 global PHP Fiber 優雅協作式多任務_處理程序_40counter++; PHP Fiber 優雅協作式多任務_客户端_41counter"); // 掛起 $counter++; // 如果另一個纖程在掛起期間修改了 $counter 會怎樣? 小心全局狀態。其他代碼(或其他纖程)可能在你掛起時修改它。
  2. 纖程創建開銷 創建纖程有少量開銷。不要創建數百萬個。與線程相比它們是輕量級的,但不是免費的。

結論:理解你的工具的力量 當你學習數據結構和算法時,你不僅僅是記憶定義和語法,你還學習何時使用它們。例如,如果你不認識何時需要在兩端快速插入 / 刪除,雙向鏈表就沒有用。

這同樣適用於語言特性。PHP 自 8.1 以來就有了纖程,但大多數開發者不使用它們,因為他們不認識纖程解決的問題。所以,下次當你面臨涉及以下問題時:

暫停和恢復執行 在乾淨的 API 背後隱藏複雜性 使異步代碼感覺同步 協作式多任務 問自己:” 纖程能優雅地解決這個問題嗎?”

你可能會驚訝於答案是肯定的頻率。

總結 PHP 纖程(Fibers)自 PHP 8.1 引入以來,一直是一個被低估的特性。通過 PHP MCP SDK 的客户端通信功能(PR #109)這個實際案例,我們看到了纖程如何優雅地解決複雜的架構問題。

這個實現的精妙之處不在於其技術複雜度,而在於其設計理念 —— 通過纖程將複雜的雙向通信機制隱藏在簡潔的同步風格 API 背後,讓用户能夠編寫直觀、易讀的代碼,而無需關心底層的掛起、恢復和狀態管理。

關鍵要點 纖程不是異步機制,而是協作式多任務的實現,是管理執行流的工具 正確的抽象層次:讓複雜性在庫層面解決一次,所有用户受益 傳輸無關性:同一套纖程機制可以適配不同的 I/O 模型(阻塞、非阻塞、多進程) 用户友好:最好的技術是讓問題對用户消失的技術 適用場景 纖程最適合:

構建庫和框架 隱藏異步複雜性 需要暫停和恢復執行上下文的場景 橋接同步和異步代碼 不適用於:

簡單的線性流程 需要真正並行性的場景 可以用簡單回調或生成器解決的問題 選擇正確的工具,理解工具的本質,是構建優雅軟件的關鍵。PHP 纖程正是這樣一個被低估但極其強大的工具。