コルーチン

webmanはWorkermanを基に開発されているため、webmanはWorkermanのコルーチン機能を利用できます。
コルーチンはSwooleSwowFiberの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.phpeventLoopを設定してコルーチンのドライバを指定できます。

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');
    }

}

eventLoopSwooleSwowFiberのいずれかである場合、webmanは各リクエストに対してコルーチンを作成して実行し、リクエスト処理中に新しいコルーチンを作成してビジネスコードを実行できます。

コルーチンの制限

  • SwooleまたはSwowをドライバとして使用している場合、ビジネスロジックがブロッキングIOに遭遇すると、コルーチンは自動的に切り替わり、同期コードを非同期で実行できます。
  • Fiberドライバを使用している場合、ブロッキングIOに遭遇するとコルーチンは切り替わることはなく、プロセスはブロッキング状態になります。
  • コルーチンを使用するときは、同一のリソースに対して複数のコルーチンが同時に操作を行ってはなりません。たとえば、データベース接続やファイル操作など、リソース競合を引き起こす可能性があります。正しい使い方は、接続プールやロックを使用してリソースを保護することです。
  • コルーチンを使用するときは、リクエスト関連のステータスデータをグローバル変数または静的変数に保存してはいけません。これによりグローバルデータ汚染が発生する可能性があります。正しい使い方は、コルーチンコンテキストcontextを使用してそれらを保存または取得することです。

その他の注意事項

Swowは内部でPHPのブロッキング関数を自動的にフックしますが、このフックはPHPのいくつかのデフォルト動作に影響を与えるため、Swowを使用していないのにSwowをインストールしている場合はバグが発生する可能性があります。

したがって、推奨事項:

  • プロジェクトでSwowを使用していない場合は、Swow拡張をインストールしないでください。
  • プロジェクトでSwowを使用している場合は、eventLoopWorkerman\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つのリクエストの結果がそれぞれ lileihanmeimei であることを期待しますが、実際にはどちらも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; 
      }
  }
}