協程是什麼
協程可以了解為純使用者态的線程,其通過協作而不是搶占來進行切換,相對于程序或者線程,協程所有的操作都可以在使用者态完成,建立和切換的消耗更低,Swoole 可以為每一個請求建立對應的協程,根據 IO 的狀态來合理的排程協程。
在 Swoole 4.x 中,協程(Coroutine)取代了異步回調,成為 Swoole推薦的程式設計方式。Swoole 協程解決了異步回調程式設計困難的問題,使用協程可以以傳統同步程式設計的方法編寫代碼,底層自動切換為異步 IO,既保證了程式設計的簡單性,又可借助異步 IO,提升系統的并發能力。
注:Swoole 4.x 之前的版本也支援協程,不過 4.x 版本對協程核心進行了重構,功能更加強大,提供了完整的協程+通道特性,帶來全新的 CSP 程式設計模型。
基本使用示例
- PHP 版本要求:>= 7.0;
- 基于
、Server
、HttpServer
進行開發的時候,Swoole 底層會在WebSocketServer
、onRequest
、onReceive
等事件回調之前自動建立一個協程,在回調函數中即可使用協程 API;onConnect
- 你也可以使用
或Coroutine::create
方法建立協程,在建立的協程中使用協程 API 進行程式設計。go
以 Swoole 自帶的 TCP 伺服器
SwooleServer
實作為例,我們可以定義伺服器端實作如下:
$server = new SwooleServer("127.0.0.1", 9501);
// 調用 onReceive 事件回調函數時底層會自動建立一個協程
$server->on('receive', function ($serv, $fd, $from_id, $data) {
// 向用戶端發送資料後關閉連接配接(在這裡面可以調用 Swoole 協程 API)
$serv->send($fd, 'Swoole: ' . $data);
$serv->close($fd);
});
$server->start();
然後我們以協程方式實作 TCP 用戶端如下:
// 通過 go 函數建立一個協程
go(function () {
$client = new SwooleCoroutineClient(SWOOLE_SOCK_TCP);
// 嘗試與指定 TCP 服務端建立連接配接,這裡會觸發 IO 事件切換協程,交出控制權讓 CPU 去處理其他事情
if ($client->connect("127.0.0.1", 9501, 0.5)) {
// 建立連接配接後發送内容
$client->send("hello worldn");
// 列印接收到的消息(調用 recv 函數會恢複協程繼續處理後續代碼,比如列印消息、關閉連接配接)
echo $client->recv();
// 關閉連接配接
$client->close();
} else {
echo "connect failed.";
}
});
底層實作原理
我們以 MySQL 連接配接查詢為例,對 Swoole 協程底層實作做一個簡單的介紹:
$server = new SwooleHttpServer('127.0.0.1', 9501, SWOOLE_BASE);
#1
$server->on('Request', function($request, $response) {
$mysql = new SwooleCoroutineMySQL();
#2
$res = $mysql->connect([
'host' => '127.0.0.1',
'user' => 'root',
'password' => 'root',
'database' => 'test',
]);
#3
if ($res == false) {
$response->end("MySQL connect fail!");
return;
}
$ret = $mysql->query('show tables', 2);
$response->end("swoole response is ok, result=".var_export($ret, true));
});
$server->start();
在這段代碼中,我們啟動一個基于 Swoole 實作的 HTTP 伺服器監聽用戶端請求,如果有
onRequest
事件發生,則通過基于 Swoole 協程實作的異步 MySQL 用戶端元件對 MySQL 伺服器發起連接配接請求,并執行查詢操作,然後将結果以響應方式傳回給 HTTP 用戶端,下面我們來看一下協程在這段代碼中的應用:
- 調用
的SwooleHttpServer
事件回調函數時,底層會調用 C 函數onRequest
建立一個協程(coro_create
位置),同時儲存這個時間點的 CPU 寄存器狀态和 ZendVM 堆棧資訊;#1
- 調用
時會發生 IO 操作,底層會調用 C 函數mysql->connect
儲存目前協程的狀态,包括 ZendVM 上下文以及協程描述資訊,并調用coro_save
讓出程式控制權,目前的請求會挂起(coro_yield
位置);#2
- 協程讓出程式控制權後,會繼續進入 HTTP 伺服器的事件循環處理其他事件,這時 Swoole 可以繼續去處理其他用戶端發來的請求;
- 當資料庫 IO 事件完成後,MySQL 連接配接成功或失敗,底層調用 C 函數
恢複對應的協程,恢複 ZendVM 上下文,繼續向下執行 PHP 代碼(coro_resume
位置);#3
-
的執行過程與mysql->query
一樣,也會觸發 IO 事件并進行一次協程切換排程;mysql->connect
- 所有操作完成後,調用
方法傳回結果,并銷毀此協程。end
注:更深層次的協程底層實作可以參考 Swoole 官方文檔的介紹。
上面這段代碼我們借助了 Swoole 實作的協程 MySQL 用戶端(Swoole 還提供了很多其他協程用戶端,如 Redis、HTTP等,後面我們會詳細介紹),所有的編碼和之前編寫同步代碼時并沒有任何不同,但是 Swoole 底層會在 IO 事件發生時,儲存目前狀态,将程式控制權交出,以便 CPU 處理其它事件,當 IO 事件完成時恢複并繼續執行後續邏輯,進而實作異步 IO 的功能,這正是協程的強大之處,它可以讓伺服器同時可以處理更多請求,而不會阻塞在這裡等待 IO 事件處理完成,進而極大提高系統的并發性。
協程的适用場景
通過上面這個簡單的示例,我們得出協程非常适合并發程式設計,常見的并發程式設計場景如下:
- 高并發服務,如秒殺系統、高性能 API 接口、RPC 伺服器,使用協程模式,服務的容錯率會大大增加,某些接口出現故障時,不會導緻整個服務崩潰;
- 爬蟲,可實作非常強大的并發能力,即使是非常慢速的網絡環境,也可以高效地利用帶寬;
- 即時通信服務,如 IM 聊天、遊戲伺服器、物聯網、消息伺服器等等,可以確定消息通信完全無阻塞,每個消息包均可即時地被處理。
協程引入的問題
協程再為我們帶來便利的同時,也引入了一些新的問題:
- 協程需要為每個并發儲存棧記憶體并維護對應的虛拟機狀态,如果程式并發很大可能會占用大量記憶體;
- 協程排程會增加額外的一些 CPU 開銷。
盡管如此,在處理高并發應用時,使用協程帶來的優勢還是遠遠高于 PHP 預設的同步阻塞機制。
協程 vs 線程
Swoole 的協程在底層實作上是單線程的,是以同一時間隻有一個協程在工作,協程的執行是串行的,這與線程不同,多個線程會被作業系統排程到多個 CPU 并行執行。
一個協程正在運作時,其他協程會停止工作。目前協程執行阻塞 IO 操作時會挂起,底層排程器會進入事件循環。當有 IO 完成事件時,底層排程器恢複事件對應的協程的執行。
在 Swoole 中對 CPU 多核的利用,仍然依賴于 Swoole 引擎的多程序機制。
協程 vs 生成器
一些架構中會使用 PHP 的生成器來實作半自動化的協程,但在實際使用中,開發者需要在涉及協程邏輯的函數調用前增加
yield
關鍵字,這帶來了額外的學習和維護成本,非常容易犯錯,此外 Yield/Generator 代碼風格與傳統的同步風格代碼存在沖突,無法複用已有代碼。
Swoole 協程是全自動化的協程,開發者無需添加任何關鍵字,底層自動實作協程的切換和排程,此外,Swoole 協程風格與傳統的同步風格代碼是一緻的,是以可以複用已有代碼。
使用時的注意事項
程式設計範式- 協程之間通訊不要使用全局變量或者引用外部變量到目前作用域,而要使用
(後面會介紹具體使用)Channel
- 項目中如果有擴充 hook 了
或者zend_execute_ex
這兩個函數,需要特别注意一下 C 棧,可以使用zend_execute_internal
重新設定 C 棧大小co::set
由于某些跟蹤調試的 PHP 擴充大量使用了全局變量,可能會導緻 Swoole 協程發生崩潰,請關閉這些相關擴充:
- xdebug
- phptrace
- aop
- molten
- xhprof
- phalcon(Swoole協程無法運作在 phalcon 架構中)
由于多個協程是并發執行的,是以以下行為可能會導緻協程出現嚴重錯誤:
- 不能使用類靜态變量/全局變量儲存協程上下文内容,否則可能導緻變量被污染,要使用 Context 管理上下文
- 同一時間可能會有很多個請求在并行處理,多個協程共用一個用戶端連接配接的話,就會導緻不同協程之間發生資料錯亂
錯誤和異常處理
在協程程式設計中可直接使用
try/catch
處理異常,但必須在協程内捕獲,不得跨協程捕獲異常。
此外,如果在協程内使用
exit
終止程式執行退出目前協程的話,會抛出
SwooleExitException
異常,你可以在需要的位置捕獲該異常并實作與原生 PHP 一樣的退出邏輯:
go(function () {
try {
SwooleCoroutine::sleep(1); // 模拟 IO 事件讓出控制權
exit(SWOOLE_EXIT_IN_COROUTINE);
} catch (SwooleExitException $exception) {
assert($exception->getStatus() === 1);
assert($exception->getFlags() === SWOOLE_EXIT_IN_COROUTINE);
return;
}
});
注:不能将 go 函數放到 try 語句塊中,這樣就是跨協程捕獲異常了。 以上内容希望幫助到大家, 很多PHPer在進階的時候總會遇到一些問題和瓶頸,業務代碼寫多了沒有方向感,不知道該從那裡入手去提升,對此我整理了一些資料,包括但不限于:分布式架構、高可擴充、高性能、高并發、伺服器性能調優、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優化、shell腳本、Docker、微服務、Nginx等多個知識點進階進階幹貨需要的可以免費分享給大家 ,需要請戳這裡連結 或者 知乎專欄
PHP7進階架構師zhuanlan.zhihu.com