Coroutine

Webman basiert auf Workerman, daher kann Webman die Coroutine-Funktionalitäten von Workerman nutzen.
Die Coroutinen unterstützen die Treiber Swoole, Swow und Fiber.

Voraussetzungen

  • PHP >= 8.1
  • Workerman >= 5.1.0 (composer require workerman/workerman ~v5.1)
  • webman-framework >= 2.1 (composer require workerman/webman-framework ~v2.1)
  • Installation der Swoole- oder Swow-Erweiterungen oder composer require revolt/event-loop (Fiber)
  • Coroutinen sind standardmäßig deaktiviert und müssen separat durch eventLoop aktiviert werden

Aktivierungsanleitung

Webman unterstützt das Aktivieren verschiedener Treiber für unterschiedliche Prozesse, sodass Sie in config/process.php den Coroutine-Treiber über eventLoop konfigurieren können:

return [
    'webman' => [
        'handler' => Http::class,
        'listen' => 'http://0.0.0.0:8787',
        'count' => 1,
        'user' => '',
        'group' => '',
        'reusePort' => false,
        'eventLoop' => '', // Standardmäßig leer, automatisch Select oder Event wählen, Coroutinen sind nicht aktiviert
        '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,
        // Um Coroutinen zu aktivieren, muss es auf Workerman\Events\Swoole::class oder Workerman\Events\Swow::class oder Workerman\Events\Fiber::class gesetzt werden
        'eventLoop' => Workerman\Events\Swoole::class,
        'context' => [],
        'constructor' => [
            'requestClass' => Request::class,
            'logger' => Log::channel('default'),
            'appPath' => app_path(),
            'publicPath' => public_path()
        ]
    ]

    // ... Weitere Konfigurationen weggelassen ...
];

Hinweis
Webman kann für verschiedene Prozesse unterschiedliche eventLoops festlegen, was bedeutet, dass Sie gezielt für bestimmte Prozesse Coroutinen aktivieren können.
Zum Beispiel in der obigen Konfiguration ist der Dienst auf Port 8787 ohne Coroutinen aktiviert, während der Dienst auf Port 8686 Coroutinen aktiviert hat, sodass eine gemischte Bereitstellung von Coroutinen und Nicht-Coroutinen über Nginx erreicht werden kann.

Coroutine-Beispiel

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

}

Wenn eventLoop auf Swoole, Swow oder Fiber gesetzt ist, wird Webman für jede Anfrage eine Coroutine erstellen, die bei der Bearbeitung der Anfrage weiterhin neue Coroutinen zur Ausführung des Geschäftslogik-Codes erstellen kann.

Einschränkungen der Coroutine

  • Bei Verwendung von Swoole oder Swow als Treiber wechselt die Coroutine automatisch, wenn die Geschäftsanwendung auf blockierendes IO stößt, was eine asynchrone Ausführung des synchronen Codes ermöglicht.
  • Bei der Verwendung des Fiber-Treibers findet bei blockierendem IO kein Wechsel der Coroutine statt, der Prozess gerät in den Blockierungszustand.
  • Bei der Verwendung von Coroutinen dürfen mehrere Coroutinen nicht gleichzeitig auf dieselbe Ressource zugreifen, z. B. bei Datenbankverbindungen oder Dateioperationen, da dies zu Ressourcenwettbewerb führen kann. Die richtige Vorgehensweise ist die Verwendung eines Verbindungspools oder von Sperren, um die Ressource zu schützen.
  • Bei der Verwendung von Coroutinen sollten anfragebezogene Zustandsdaten nicht in globalen oder statischen Variablen gespeichert werden, da dies zur Verschmutzung globaler Daten führen kann. Die richtige Vorgehensweise besteht darin, den Coroutine-Kontext context zu verwenden, um sie zu speichern und abzurufen.

Weitere Hinweise

Swow hookt automatisch blockierende PHP-Funktionen, jedoch kann dieser Hook das Standardverhalten von PHP beeinträchtigen. Daher kann es zu Bugs kommen, wenn Sie Swow nicht verwenden, aber es installiert haben.

Deshalb wird empfohlen:

  • Wenn Ihr Projekt Swow nicht verwendet, installieren Sie bitte die Swow-Erweiterung nicht.
  • Wenn Ihr Projekt Swow verwendet, setzen Sie eventLoop auf Workerman\Events\Swow::class.

Coroutine-Kontext

Im Coroutine-Umfeld ist es untersagt, anfragebezogene Statusinformationen in globalen oder statischen Variablen zu speichern, da dies zu einer Verschmutzung globaler Variablen führen kann, wie in folgendem Beispiel:

<?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;
    }
}

Hinweis
Im Coroutine-Umfeld ist die Verwendung von globalen oder statischen Variablen nicht untersagt, sondern es ist untersagt, globale oder statische Variablen für anfragebezogene Statusdaten zu verwenden.
Beispielsweise wird empfohlen, globale Variablen oder statische Variablen für allgemein freigegebene Objektdaten wie globale Konfigurationen, Datenbankverbindungen und Instanzen bestimmter Klassen zu verwenden.

Wenn die Anzahl der Prozesse auf 1 gesetzt ist und wir zwei Anfragen nacheinander senden:
http://127.0.0.1:8787/test?name=lilei
http://127.0.0.1:8787/test?name=hanmeimei
erwarten wir, dass die Ergebnisse der beiden Anfragen lilei und hanmeimei sind, aber tatsächlich wird nur hanmeimei zurückgegeben.
Dies liegt daran, dass die zweite Anfrage die statische Variable $name überschreibt, und als die erste Anfrage aufwacht, ist der Wert der statischen Variable $name bereits hanmeimei.

Die richtige Vorgehensweise wäre, den Kontext zur Speicherung anfragerelevanter Statusdaten zu verwenden.

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

Die support\Context-Klasse wird verwendet, um Daten im Coroutine-Kontext zu speichern. Nach Abschluss der Coroutine werden die entsprechenden Kontextdaten automatisch gelöscht.
Im Coroutine-Umfeld ist jede Anfrage eine separate Coroutine, sodass die Kontextdaten nach Abschluss der Anfrage automatisch zerstört werden.
Im Nicht-Coroutine-Umfeld werden Kontextdaten am Ende der Anfrage ebenfalls automatisch gelöscht.

Lokale Variablen verursachen keine Datenverschmutzung

<?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;
    }
}

Da $name eine lokale Variable ist, können Coroutinen nicht auf lokale Variablen von anderen Coroutinen zugreifen, sodass die Verwendung lokaler Variablen coroutine-sicher ist.

Locker-Sperre

Manchmal sind einige Komponenten oder Geschäftslogiken nicht auf die Coroutine-Umgebung ausgelegt, was zu Ressourcenwettbewerb oder Atomaritätsproblemen führen kann. In diesem Fall kann Workerman\Locker verwendet werden, um eine Sperre zu setzen und die Verarbeitung der Anfragen zu regeln, um parallele Probleme zu vermeiden.

<?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);
        }
        // Wenn keine Sperre gesetzt ist, wird unter Swoole ein Fehler wie "Socket#10 has already been bound to another coroutine#10" ausgelöst
        // Unter Swow kann es zu einem Coredump kommen
        // Bei Fiber kommt es aufgrund der synchronen blockierenden IO des Redis-Extensions nicht zu Problemen
        Locker::lock('redis');
        $time = $redis->time();
        Locker::unlock('redis');
        return json($time);
    }

}

Parallel - Parallelverarbeitung

Wenn wir mehrere Aufgaben parallel ausführen und Ergebnisse abrufen möchten, können wir Workerman\Parallel verwenden.

<?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) {
                // Etwas tun
                return $i;
            });
        }
        $results = $parallel->wait();
        return json($results); // Antwort: [1,2,3,4]
    }

}

Pool - Verbindungspool

Wenn mehrere Coroutinen dieselbe Verbindung gemeinsam nutzen, kann es zu Datenverwirrung kommen. Daher ist es notwendig, Verbindungspools zu verwenden, um Datenbank-, Redis- oder andere Verbindungsressourcen zu verwalten.

Webman hat bereits Komponenten wie webman/database, webman/redis, webman/cache, webman/think-orm, webman/think-cache usw. bereitgestellt, die alle Verbindungspools integriert haben und die Nutzung in Coroutine- und Nicht-Coroutine-Umgebungen unterstützen.

Wenn Sie eine Komponente ohne Verbindungspool umgestalten möchten, können Sie Workerman\Pool verwenden, um dies zu realisieren. Hier ist ein Referenzbeispiel:

Datenbankkomponente

<?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();
        }
        // Aus dem Coroutine-Kontext die Verbindung abrufen, um sicherzustellen, dass dieselbe Coroutine dieselbe Verbindung verwendet
        $pdo = Context::get('pdo');
        if (!$pdo) {
            // Verbindung aus dem Pool abrufen
            $pdo = self::$pool->get();
            Context::set('pdo', $pdo);
            // Wenn die Coroutine beendet wird, die Verbindung automatisch zurückgeben
            Coroutine::defer(function () use ($pdo) {
                self::$pool->put($pdo);
            });
        }
        return call_user_func_array([$pdo, $name], $arguments);
    }

    private static function initializePool(): void
    {
        // Einen Verbindungspool mit maximal 10 Verbindungen erstellen
        self::$pool = new Pool(10);
        // Verbindungsersteller festlegen (zur Vereinfachung wurde das Einlesen der Konfigurationsdatei weggelassen)
        self::$pool->setConnectionCreator(function () {
            return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
        });
        // Verbindungsbehandler festlegen
        self::$pool->setConnectionCloser(function ($pdo) {
            $pdo = null;
        });
        // Herzschlagprüfer festlegen
        self::$pool->setHeartbeatChecker(function ($pdo) {
            $pdo->query('SELECT 1');
        });
    }

}

Verwendung

<?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"}]
    }

}

Weitere Informationen zu Coroutinen und verwandten Komponenten

Siehe workerman Coroutine-Dokumentation.

Mischung von Coroutine- und Nicht-Coroutine-Bereitstellungen

Webman unterstützt die gemischte Bereitstellung von Coroutinen und Nicht-Coroutinen, beispielsweise können Nicht-Coroutinen für normale Geschäftsabläufe verwendet werden, während Coroutinen für langsame IO-Geschäfte eingesetzt werden, indem Anfragen über Nginx auf unterschiedliche Dienste weitergeleitet werden.

Beispielsweise in config/process.php

return [
    'webman' => [
        'handler' => Http::class,
        'listen' => 'http://0.0.0.0:8787',
        'count' => 1,
        'user' => '',
        'group' => '',
        'reusePort' => false,
        'eventLoop' => '', // Standardmäßig leer, automatisch Select oder Event wählen, Coroutinen sind nicht aktiviert
        '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,
        // Um Coroutinen zu aktivieren, muss es auf Workerman\Events\Swoole::class oder Workerman\Events\Swow::class oder Workerman\Events\Fiber::class gesetzt werden
        'eventLoop' => Workerman\Events\Swoole::class,
        'context' => [],
        'constructor' => [
            'requestClass' => Request::class,
            'logger' => Log::channel('default'),
            'appPath' => app_path(),
            'publicPath' => public_path()
        ]
    ],

    // ... Weitere Konfigurationen weggelassen ...
];

Dann konfigurieren Sie Nginx, um Anfragen an unterschiedliche Dienste weiterzuleiten.

upstream webman {
    server 127.0.0.1:8787;
    keepalive 10240;
}

# Fügen Sie einen neuen Upstream für 8686 hinzu
upstream task {
   server 127.0.0.1:8686;
   keepalive 10240;
}

server {
  server_name webman.com;
  listen 80;
  access_log off;
  root /path/webman/public;

  # Anfragen, die mit /tast beginnen, gehen an Port 8686. Bitte passen Sie /tast entsprechend an.
  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;
  }

  # Alle anderen Anfragen gehen an den ursprünglichen Port 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;
      }
  }
}