Coroutine
Webman est développé sur la base de Workerman, donc Webman peut utiliser les caractéristiques de coroutine de Workerman.
Les coroutines prennent en charge trois pilotes : Swoole
, Swow
et Fiber
.
Conditions requises
- 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 ou Swow installé, ou installer
composer require revolt/event-loop
(Fiber) - Les coroutines sont désactivées par défaut et doivent être activées en configurant
eventLoop
Méthode d'activation
Webman prend en charge l'activation de différents pilotes pour différents processus, vous pouvez donc configurer le pilote de coroutine dans config/process.php
via eventLoop
:
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // Par défaut vide, sélectionne automatiquement Select ou Event, ne pas activer les coroutines
'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,
// Pour activer les coroutines, il faut définir comme Workerman\Events\Swoole::class ou Workerman\Events\Swow::class ou Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
]
// ... Autres configurations omises ...
];
Remarque
Webman peut définir différentseventLoop
pour différents processus, ce qui signifie que vous pouvez activer sélectivement les coroutines pour des processus spécifiques.
Par exemple, le service sur le port 8787 ci-dessus n’a pas de coroutine activée, tandis que le service sur le port 8686 a une coroutine activée, ce qui permet de déployer des coroutines et des non-coroutines en mélange.
Exemple de coroutine
<?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');
}
}
Lorsque eventLoop
est Swoole
, Swow
ou Fiber
, Webman créera une coroutine pour chaque requête pour s'exécuter, et pendant le traitement de la requête, il peut continuer à créer de nouvelles coroutines pour exécuter le code métier.
Limites des coroutines
- Lors de l'utilisation de Swoole ou Swow comme pilote, les coroutines se déplaceront automatiquement en cas d'IO bloquante, permettant ainsi l'exécution asynchrone du code synchrone.
- Lors de l'utilisation de Fiber comme pilote, lorsqu'une IO bloquante est rencontrée, il n'y aura pas de changement de coroutine, et le processus entrera dans un état bloquant.
- Lors de l'utilisation de coroutines, il ne peut pas y avoir plusieurs coroutines effectuant des opérations simultanément sur la même ressource, comme une connexion à une base de données, des opérations sur des fichiers, etc. Cela pourrait entraîner une concurrence pour les ressources. La bonne pratique consiste à utiliser un pool de connexions ou un verrou pour protéger les ressources.
- Lors de l'utilisation de coroutines, il ne faut pas stocker les données d'état liées à la requête dans des variables globales ou statiques, car cela pourrait entraîner une pollution des données globales. La bonne pratique consiste à utiliser le contexte de coroutine
context
pour les stocker et les récupérer.
Autres considérations
Swow héberge automatiquement les fonctions bloquantes de PHP, mais en raison de cela, cela impacte certains comportements par défaut de PHP. Ainsi, si vous n’utilisez pas Swow mais que vous l’avez installé, cela pourrait causer des bugs.
Par conséquent, il est recommandé de :
- Si votre projet n'utilise pas Swow, veuillez ne pas installer l'extension Swow.
- Si votre projet utilise Swow, veuillez définir
eventLoop
surWorkerman\Events\Swow::class
.
Contexte de coroutine
Dans l'environnement de coroutine, il est interdit de stocker des informations d'état liées aux requêtes dans des variables globales ou statiques, car cela pourrait provoquer une pollution des variables globales, par exemple :
<?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;
}
}
Attention
Dans un environnement de coroutine, il n'est pas interdit d'utiliser des variables globales ou statiques, mais il est interdit d'utiliser des variables globales ou statiques pour stocker des données d'état liées aux requêtes.
Par exemple, les configurations globales, les connexions à la base de données ou certains objets de type singleton qui doivent être partagés globalement sont recommandés pour être stockés dans des variables globales ou statiques.
En réglant le nombre de processus à 1, lorsque nous émettons consécutivement deux requêtes
http://127.0.0.1:8787/test?name=lilei
http://127.0.0.1:8787/test?name=hanmeimei
Nous nous attendons à ce que les deux requêtes retournent les résultats respectifs de lilei
et hanmeimei
, mais en réalité, elles retournent toutes deux hanmeimei
.
C'est parce que la deuxième requête a écrasé la variable statique $name
. Lorsque la première requête se termine, la variable statique $name
est déjà devenue hanmeimei
.
La bonne méthode consiste à utiliser le context pour stocker les données d'état des requêtes
<?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');
}
}
La classe support\Context
est utilisée pour stocker les données de contexte de coroutine. Lorsque l'exécution de la coroutine est terminée, les données contextuelles correspondantes seront automatiquement supprimées.
Dans l'environnement de coroutine, chaque requête étant une coroutine distincte, les données de contexte seront automatiquement détruites à la fin de la requête.
Dans un environnement non-coroutine, le contexte sera automatiquement détruit à la fin de la requête.
Les variables locales ne causeront pas de pollution des données
<?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;
}
}
Puisque $name
est une variable locale, les coroutines ne peuvent pas accéder aux variables locales de l'autre, il est donc sûr d'utiliser des variables locales dans les coroutines.
Locker Verrou
Parfois, certains composants ou processus n'ont pas pris en compte l'environnement de coroutine, ce qui peut entraîner des problèmes de concurrence ou d'atome. Dans ce cas, vous pouvez utiliser Workerman\Locker
pour implémenter un verrou et gérer les tâches dans un ordre séquentiel afin d'éviter les problèmes de concurrence.
<?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);
}
// Si vous n'ajoutez pas de verrou, sous Swoole cela déclenchera une erreur du type "Socket#10 has already been bound to another coroutine#10"
// Sous Swow, cela peut provoquer un coredump
// Sous Fiber, il n'y a pas de problème car l'extension Redis est synchrone en IO bloquante
Locker::lock('redis');
$time = $redis->time();
Locker::unlock('redis');
return json($time);
}
}
Exécution parallèle
Lorsque nous devons exécuter plusieurs tâches en parallèle et obtenir des résultats, nous pouvons utiliser Workerman\Parallel
pour le faire.
<?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) {
// Faites quelque chose
return $i;
});
}
$results = $parallel->wait();
return json($results); // Response: [1,2,3,4]
}
}
Pool Pool de connexions
Plusieurs coroutines partageant la même connexion peuvent entraîner une confusion des données, il est donc nécessaire d’utiliser des pools de connexions pour gérer les ressources de connexion à des bases de données, Redis, etc.
Webman a déjà fourni des composants tels que webman/database, webman/redis, webman/cache, webman/think-orm, webman/think-cache, qui tous intègrent des pools de connexion et prennent en charge leur utilisation dans des environnements de coroutine et non-coroutine.
Si vous souhaitez modifier un composant sans pool de connexion, vous pouvez utiliser Workerman\Pool
pour le faire, comme montré dans le code suivant.
Composant de base de données
<?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();
}
// Obtenez la connexion depuis le contexte de coroutine pour garantir que la même coroutine utilise la même connexion
$pdo = Context::get('pdo');
if (!$pdo) {
// Obtenez la connexion depuis le pool de connexions
$pdo = self::$pool->get();
Context::set('pdo', $pdo);
// Lorsque la coroutine se termine, la connexion est automatiquement restituée
Coroutine::defer(function () use ($pdo) {
self::$pool->put($pdo);
});
}
return call_user_func_array([$pdo, $name], $arguments);
}
private static function initializePool(): void
{
// Créez un pool de connexions, avec un nombre maximum de connexions de 10
self::$pool = new Pool(10);
// Définir le créateur de connexion (pour des raisons de simplicité, la lecture du fichier de configuration est omise)
self::$pool->setConnectionCreator(function () {
return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
});
// Définir le fermant de connexion
self::$pool->setConnectionCloser(function ($pdo) {
$pdo = null;
});
// Définir le vérificateur de heartbeat
self::$pool->setHeartbeatChecker(function ($pdo) {
$pdo->query('SELECT 1');
});
}
}
Utilisation
<?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"}]
}
}
Plus d'introduction sur les coroutines et composants connexes
Référez-vous à la documentation des coroutines Workerman
Déploiement mixte de coroutines et de non-coroutines
Webman prend en charge le déploiement mixte de coroutines et de non-coroutines, par exemple, le traitement des affaires ordinaires sans coroutine et le traitement des affaires IO lentes avec des coroutines, en redirigeant les requêtes vers différents services via Nginx.
Par exemple config/process.php
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // Par défaut vide, sélectionne automatiquement Select ou Event, ne pas activer les coroutines
'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,
// Pour activer les coroutines, il faut définir comme Workerman\Events\Swoole::class ou Workerman\Events\Swow::class ou Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
// ... Autres configurations omises ...
];
Puis configurez Nginx pour rediriger les requêtes vers différents services
upstream webman {
server 127.0.0.1:8787;
keepalive 10240;
}
# Ajouter un nouveau upstream pour 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;
# Les requêtes commençant par /tast passent par le port 8686. Veuillez modifier /tast en fonction de vos besoins
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;
}
# Les autres requêtes passent par le 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;
}
}
}