Корутинки
Webman разрабатывается на основе Workerman, поэтому Webman может использовать корутинные характеристики Workerman.
Корутинки поддерживают три драйвера: Swoole
, Swow
и Fiber
.
Предварительные условия
- 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 поддерживает активацию различных драйверов для различных процессов, поэтому вы можете настроить корутинный драйвер через конфигурацию eventLoop
в 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()
]
]
// ... Другие конфигурации опущены ...
];
Подсказка
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, возникающие блокирующие операции ввода-вывода автоматически переключаются, что позволяет выполнять синхронный код асинхронно.
- Когда используется драйвер Fiber, в случае блокирующего ввода-вывода переключения корутин не произойдет, процесс окажется в состоянии блокировки.
- При использовании корутин нельзя одновременно обращаться к одному и тому же ресурсу нескольким корутинам, например, к подключению к базе данных или файловым операциям. Это может привести к конкуренции за ресурсы. Правильный способ — использовать пул подключений или блокировки для защиты ресурсов.
- При использовании корутин нельзя сохранять данные состояния, относящиеся к запросу, в глобальных или статических переменных, поскольку это может привести к загрязнению глобальных данных. Правильный способ — использовать контекст корутины
context
для их сохранения и получения.
Другие важные моменты
Swow автоматически перехватывает блокирующие функции PHP на нижнем уровне, но этот перехват может повлиять на некоторые представления по умолчанию. Поэтому, если вы не используете 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;
}
}
Внимание
В среде корутин не запрещено использовать глобальные или статические переменные, но запрещено использовать их для хранения состояния, относящегося к запросу.
Например, глобальные настройки, подключения к базе данных, некоторые синглтоны классов и т.д., которые необходимо делить между глобальными данными, рекомендуется хранить в глобальных или статических переменных.
При установке числа процессов на 1, когда мы последовательно отправляем два запроса
http://127.0.0.1:8787/test?name=lilei
http://127.0.0.1:8787/test?name=hanmeimei
Ожидаем, что результаты двух запросов будут lilei
и hanmeimei
соответственно, но фактически оба ответа будут hanmeimei
.
Это произошло потому, что второй запрос перезаписал статическую переменную $name
, и когда первый запрос вернулся после завершения времени сна, статическая переменная $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
используется для хранения данных контекста корутины. После завершения выполнения корутины соответствующие данные контекста будут автоматически удалены.
В среде корутин каждый запрос соответствует отдельной корутине, поэтому данные контекста автоматически уничтожаются при завершении запроса.
В некорутинной среде контекст автоматически уничтожается по завершении запроса.
Локальные переменные не приведут к загрязнению данных
<?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
является локальной переменной, корутины не могут взаимно обращаться к локальным переменным, поэтому использование локальных переменных безопасно для корутин.
Locker (Блокировка)
Иногда некоторые компоненты или бизнес-логику не учитывают корутинные окружения, что может привести к проблемам с конкуренцией ресурсов или атомарностью. В этом случае можно использовать 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 это может вызвать core dump
// В 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); // Ответ: [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 поддерживает смешанное развертывание корутин и некорутин, например, некорутинная обработка обычных задач и корутинная обработка медленных задач ввода-вывода. Запросы перенаправляются через 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;
}
# Добавить новый upstream на 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;
}
}
}