協程篇 | Workerman 5.x 協程上下文 Context
協程
協程是一種比線程更輕量級的用戶級并發機制,能夠在進程中實現多任務調度。它通過手動控制掛起和恢復來實現協程間的切換,避免了進程上下文切換的開銷。workerman提供了一個通用的協程接口,底層自動兼容Swoole/Swow/Fiber驅動。
提示:此特性需要 workerman>=5.1.0
注意
- 協程僅支持Swoole Swow Fiber驅動
- 如果使用Fiber驅動時需要安裝 composer require revolt/event-loop
- Swoole或者Swow驅動可以實現PHP阻塞函數自動協程化,從而實現原來的同步代碼異步執行
- 但Fiber無法像Swoole和Swow那樣自動協程化,遇到PHP自帶的阻塞函數時會阻塞整個進程,并不會發生協程切換
- 當使用Swoole Swow Fiber驅動時,workerman每次運行onWorkerStart onMessage onConnect onClose等回調時會自動創建一個協程來執行
- 可以利用$worker->eventLoop=xxx;給不同worker設置不同的協程驅動
案例
coroutine.php文件
<?php
declare(strict_types=1);
use Workerman\Connection\TcpConnection;
use Workerman\Coroutine;
use Workerman\Events\Swoole;
use Workerman\Events\Fiber;
use Workerman\Protocols\Http\Request;
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
$worker1 = new Worker('http://0.0.0.0:8201');
$worker1->eventLoop = Swoole::class; // 使用Swoole協程
$worker1->onMessage = function (TcpConnection $connection, Request $request) {
Coroutine::create(function () {
echo file_get_contents("http://www.baidu.com");
});
$connection->send('Swoole ok');
};
$worker2 = new Worker('http://0.0.0.0:8202');
$worker2->eventLoop = Fiber::class; // 使用自帶的Fiber協程
$worker2->onMessage = function (TcpConnection $connection, Request $request) {
Coroutine::create(function () {
echo file_get_contents("http://www.baidu.com");
});
$connection->send('Fiber ok');
};
Worker::runAll();
啟動服務
php coroutine.php start
Workerman[worker.php] start in DEBUG mode
-------------------------------------------- WORKERMAN ---------------------------------------------
Workerman/5.1.0 PHP/8.3.15 (Jit off) Linux/5.10.102.1-microsoft-standard-WSL2
--------------------------------------------- WORKERS ----------------------------------------------
event-loop proto user worker listen count state
swoole tcp root none http://0.0.0.0:8201 1 [OK]
fiber tcp root none http://0.0.0.0:8202 1 [OK]
----------------------------------------------------------------------------------------------------
Press Ctrl+C to stop. Start success.
訪問
- Swoole 服務 :http://127.0.0.1:8201
- Fiber 服務 :http://127.0.0.1:8202
協程提供的接口
interface CoroutineInterface
{
/**
* 創建協程并立即執行
*/
public static function create(callable $callable, ...$data): CoroutineInterface;
/**
* 開始協程運行
*/
public function start(mixed ...$args): mixed;
/**
* 恢復協程運行
*/
public function resume(mixed ...$args): mixed;
/**
* 獲取協程id
*/
public function id(): int;
/**
* 設置協程銷毀時的回調
*/
public static function defer(callable $callable): void;
/**
* 暫停當前協程
*/
public static function suspend(mixed $value = null): mixed;
/**
* 獲取當前協程
*/
public static function getCurrent(): CoroutineInterface|Fiber|SwowCoroutine|static;
/**
* 判斷當前是否是協程環境
*/
public static function isCoroutine(): bool;
}
關于協程
優勢
PHP引入協程后最大的作用就是可以用同步的方式編寫異步代碼,避免了回調地獄,提高了代碼的可讀性和可維護性。
協程能大幅度提升IO密集型業務的彈性,可以用較少的進程提供更大的吞吐量。
劣勢
但是引入協程后開發者需要時刻注意全局變量污染、資源競爭、第三方庫改造等問題,開發維護成本增大,心智負擔明顯增加。
引入協程后產生了協程創建、調度、銷毀、連接池等額外開銷。通過大量壓測數據來看,在充分利用CPU的情況下,引入協程后極限性能比阻塞式IO下降約10%-20%。
Context 協程上下文
Context用于在協程中存儲和傳遞上下文信息,例如數據庫連接、用戶信息等。每個協程有自己的上下文,不同協程之間的上下文是隔離的。
案例
context.php
<?php
declare(strict_types=1);
use Workerman\Connection\TcpConnection;
use Workerman\Coroutine;
use Workerman\Coroutine\Context;
use Workerman\Events\Swoole;
use Workerman\Protocols\Http\Request;
use Workerman\Worker;
require_once __DIR__ . '/vendor/autoload.php';
$worker = new Worker('http://0.0.0.0:8201');
$worker->eventLoop = Swoole::class; // Or Swow::class or Fiber::class
$worker->onWorkerStart = function () {
Context::set('user_info_onWorkerStart', ['id' => 1, 'name' => 'onWorkerStart']);
};
$worker->onMessage = function (TcpConnection $connection, Request $request) {
// 協程間context數據是隔離的,所以onMessage協程里獲取onWorkerStart協程里的user_info_onWorkerStart是null
var_dump(Context::get('user_info_onWorkerStart'));
// 在當前協程設置context數據
Context::set('user_info', ['id' => 2025, 'name' => 'Tinywan']);
// 新建協程
Coroutine::create(function () use ($connection) {
// 協程間context數據是隔離的,所以新協程里獲取的是null
$userInfo = Context::get('user_info');
var_dump($userInfo); // 輸出null
Context::set('user_info_new', ['id' => 1, 'name' => 'New Coroutine']);
});
// 協程間context數據是隔離的,所以獲取新協程里的數據是null
var_dump(Context::get('user_info_new')); // 輸出null
// 獲取當前協程的context數據
$userInfo = Context::get('user_info'); // 得到['id' => 2025, 'name' => 'Tinywan']
$connection->send(json_encode($userInfo));
};
Worker::runAll();
啟動服務
php context.php start
訪問服務
請求地址http://127.0.0.1:8201,響應輸出
{
"id": 2025,
"name": "Tinywan"
}
接口說明
interface ContextInterface
{
/**
* 獲取上下文中的值
*/
public static function get(string $name, mixed $default = null): mixed;
/**
* 設置上下文中的值
*/
public static function set(string $name, mixed $value): void;
/**
* 檢查上下文中是否存在指定名稱的值
*/
public static function has(string $name): bool;
/**
* 重置當前協程上下文
*/
public static function reset(?ArrayObject $data = null): void;
/**
* 銷毀上下文
*/
public static function destroy(): void;
}