コルーチン
webmanはWorkermanを基に開発されているため、webmanはWorkermanのコルーチン機能を利用できます。
コルーチンはSwoole
、Swow
、Fiber
の3つのドライバをサポートしています。
前提条件
- PHP >= 8.1
- Workerman >= 5.1.0 (
composer require workerman/workerman ~v5.1
) - webman-framework >= 2.1 (
composer require workerman/webman-framework ~v2.1
) - swooleまたはswow拡張がインストールされているか、
composer require revolt/event-loop
(Fiber)をインストール - コルーチンはデフォルトで無効になっています。eventLoopを設定して有効にする必要があります。
有効化方法
webmanは異なるプロセスに異なるドライバを設定することをサポートしているため、config/process.php
でeventLoop
を設定してコルーチンのドライバを指定できます。
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // 空のままではSelectまたはEventを自動で選択し、コルーチンは無効
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
'my-coroutine' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8686',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
// コルーチンを有効にするには Workerman\Events\Swoole::class または Workerman\Events\Swow::class または Workerman\Events\Fiber::class を設定する必要があります
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
]
// ... その他の設定は省略 ...
];
ヒント
webmanは異なるプロセスに異なるeventLoopを設定できるため、特定のプロセスに選択的にコルーチンを有効にすることができます。
例えば、上記の設定では、8787ポートのサービスはコルーチンを有効にしておらず、8686ポートのサービスはコルーチンを有効にしています。nginxでリダイレクトすることで、コルーチンと非コルーチンの混合デプロイが実現できます。
コルーチンの例
<?php
namespace app\controller;
use support\Response;
use Workerman\Coroutine;
use Workerman\Timer;
class IndexController
{
public function index(): Response
{
Coroutine::create(function(){
Timer::sleep(1.5);
echo "hello coroutine\n";
});
return response('hello webman');
}
}
eventLoop
がSwoole
、Swow
、Fiber
のいずれかである場合、webmanは各リクエストに対してコルーチンを作成して実行し、リクエスト処理中に新しいコルーチンを作成してビジネスコードを実行できます。
コルーチンの制限
- SwooleまたはSwowをドライバとして使用している場合、ビジネスロジックがブロッキングIOに遭遇すると、コルーチンは自動的に切り替わり、同期コードを非同期で実行できます。
- Fiberドライバを使用している場合、ブロッキングIOに遭遇するとコルーチンは切り替わることはなく、プロセスはブロッキング状態になります。
- コルーチンを使用するときは、同一のリソースに対して複数のコルーチンが同時に操作を行ってはなりません。たとえば、データベース接続やファイル操作など、リソース競合を引き起こす可能性があります。正しい使い方は、接続プールやロックを使用してリソースを保護することです。
- コルーチンを使用するときは、リクエスト関連のステータスデータをグローバル変数または静的変数に保存してはいけません。これによりグローバルデータ汚染が発生する可能性があります。正しい使い方は、コルーチンコンテキスト
context
を使用してそれらを保存または取得することです。
その他の注意事項
Swowは内部でPHPのブロッキング関数を自動的にフックしますが、このフックはPHPのいくつかのデフォルト動作に影響を与えるため、Swowを使用していないのにSwowをインストールしている場合はバグが発生する可能性があります。
したがって、推奨事項:
- プロジェクトでSwowを使用していない場合は、Swow拡張をインストールしないでください。
- プロジェクトでSwowを使用している場合は、
eventLoop
をWorkerman\Events\Swow::class
に設定してください。
コルーチンコンテキスト
コルーチン環境ではリクエスト関連のステータス情報をグローバル変数または静的変数に保存することは禁止されています。これによりグローバル変数の汚染が発生する可能性があります。たとえば:
<?php
namespace app\controller;
use support\Request;
use Workerman\Timer;
class TestController
{
protected static $name = '';
public function index(Request $request)
{
static::$name = $request->get('name');
Timer::sleep(5);
return static::$name;
}
}
注意
コルーチン環境ではグローバル変数または静的変数の使用が禁止されているわけではなく、リクエスト関連のステータスデータをグローバル変数または静的変数に保存することが禁止されているのです。
グローバル設定、データベース接続、singletonクラスなど、共有する必要があるオブジェクトデータは、グローバル変数や静的変数に保存することを推奨します。
プロセス数を1に設定した場合、連続して2つのリクエストを行うと
http://127.0.0.1:8787/test?name=lilei
http://127.0.0.1:8787/test?name=hanmeimei
私たちは、2つのリクエストの結果がそれぞれ lilei
と hanmeimei
であることを期待しますが、実際にはどちらもhanmeimei
が返されます。
これは、2番目のリクエストが静的変数$name
を上書きしてしまったためです。1番目のリクエストが睡眠から戻ったときには、静的変数$name
はすでにhanmeimei
になっていました。
正しい方法は、contextを使用してリクエストステータスデータを保存することです。
<?php
namespace app\controller;
use support\Request;
use support\Context;
use Workerman\Timer;
class TestController
{
public function index(Request $request)
{
Context::set('name', $request->get('name'));
Timer::sleep(5);
return Context::get('name');
}
}
support\Context
クラスはコルーチンコンテキストデータを保存するために使用されます。コルーチンの実行が完了すると、対応するcontextデータは自動的に削除されます。
コルーチン環境では、各リクエストが個別のコルーチンであるため、リクエストが完了するとcontextデータは自動的に破棄されます。非コルーチン環境では、contextはリクエストの終了時に自動的に破棄されます。
ローカル変数はデータ汚染を引き起こさない
<?php
namespace app\controller;
use support\Request;
use support\Context;
use Workerman\Timer;
class TestController
{
public function index(Request $request)
{
$name = $request->get('name');
Timer::sleep(5);
return $name;
}
}
$name
がローカル変数であるため、コルーチン間でローカル変数に相互にアクセスすることはできないため、ローカル変数の使用はコルーチンの安全性を確保します。
ロッカー
時には、いくつかのコンポーネントやビジネスがコルーチン環境を考慮していないため、リソース競合や原子性の問題が発生する可能性があります。このような場合には、Workerman\Locker
を使用してロックし、順次処理を実現して並行問題を防ぎます。
<?php
namespace app\controller;
use Redis;
use support\Response;
use Workerman\Coroutine\Locker;
class IndexController
{
public function index(): Response
{
static $redis;
if (!$redis) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
}
// ロックを加えないと、Swooleで "Socket#10 has already been bound to another coroutine#10" のようなエラーが発生します
// Swowではコアダンプが発生する可能性があります
// FiberではRedis拡張が同期ブロッキングIOであるため、問題は発生しません
Locker::lock('redis');
$time = $redis->time();
Locker::unlock('redis');
return json($time);
}
}
並行実行
複数のタスクを並行して実行し、結果を取得する必要がある場合は、Workerman\Parallel
を使用して実現できます。
<?php
namespace app\controller;
use support\Response;
use Workerman\Coroutine\Parallel;
class IndexController
{
public function index(): Response
{
$parallel = new Parallel();
for ($i=1; $i<5; $i++) {
$parallel->add(function () use ($i) {
// 何かを実行
return $i;
});
}
$results = $parallel->wait();
return json($results); // Response: [1,2,3,4]
}
}
プール(接続プール)
複数のコルーチンが同じ接続を共有すると、データ混乱が発生するため、接続プールを使用してデータベースやRedisなどの接続リソースを管理する必要があります。
webmanはすでに webman/database webman/redis webman/cache webman/think-orm webman/think-cache などのコンポーネントを提供しており、これらはすべて接続プールを統合し、コルーチン環境と非コルーチン環境の両方で使用できます。
接続プールのないコンポーネントを改造したい場合は、Workerman\Pool
を使用して実現できます。以下のコードを参照してください。
データベースコンポーネント
<?php
namespace app;
use Workerman\Coroutine\Context;
use Workerman\Coroutine;
use Workerman\Coroutine\Pool;
class Db
{
private static ?Pool $pool = null;
public static function __callStatic($name, $arguments)
{
if (self::$pool === null) {
self::initializePool();
}
// コルーチンコンテキストから接続を取得し、同一のコルーチンで同じ接続を使用することを保証
$pdo = Context::get('pdo');
if (!$pdo) {
// 接続プールから接続を取得
$pdo = self::$pool->get();
Context::set('pdo', $pdo);
// コルーチン終了時に自動的に接続を返却
Coroutine::defer(function () use ($pdo) {
self::$pool->put($pdo);
});
}
return call_user_func_array([$pdo, $name], $arguments);
}
private static function initializePool(): void
{
// 最大接続数10の接続プールを作成
self::$pool = new Pool(10);
// 接続作成器を設定(簡略化のため、設定ファイルの読み込みは省略しています)
self::$pool->setConnectionCreator(function () {
return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
});
// 接続クローズ器を設定
self::$pool->setConnectionCloser(function ($pdo) {
$pdo = null;
});
// ハートビート検査器を設定
self::$pool->setHeartbeatChecker(function ($pdo) {
$pdo->query('SELECT 1');
});
}
}
使用例
<?php
namespace app\controller;
use support\Response;
use app\Db;
class IndexController
{
public function index(): Response
{
$value = Db::query('SELECT NOW() as now')->fetchAll();
return json($value); // [{"now":"2025-02-06 23:41:03","0":"2025-02-06 23:41:03"}]
}
}
さらにコルーチン及び関連コンポーネントの紹介
workerman コルーチン文書を参照してください。
コルーチンと非コルーチンの混合デプロイ
webmanはコルーチンと非コルーチンの混合デプロイをサポートしています。たとえば、非コルーチンで通常のビジネスを処理し、コルーチンで遅いIOビジネスを処理し、nginxを通じて異なるサービスにリクエストを転送します。
例えば、config/process.php
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // 空のままではSelectまたはEventを自動で選択し、コルーチンは無効
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
'my-coroutine' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8686',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
// コルーチンを有効にするには Workerman\Events\Swoole::class または Workerman\Events\Swow::class または Workerman\Events\Fiber::class を設定する必要があります
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
// ... その他の設定は省略 ...
];
その後、nginxの設定で異なるサービスにリクエストを転送します。
upstream webman {
server 127.0.0.1:8787;
keepalive 10240;
}
# 8686アップストリームを追加
upstream task {
server 127.0.0.1:8686;
keepalive 10240;
}
server {
server_name webman.com;
listen 80;
access_log off;
root /path/webman/public;
# /tastで始まるリクエストは8686ポートへ、実際の必要に応じて/tastを変更してください
location /tast {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://task;
}
# その他のリクエストは元の8787ポートへ
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
if (!-f $request_filename){
proxy_pass http://webman;
}
}
}