Coroutine
Webman is developed based on Workerman, so Webman can utilize the coroutine features of Workerman.
Coroutines support three drivers: Swoole
, Swow
, and Fiber
.
Prerequisites
- PHP >= 8.1
- Workerman >= 5.1.0 (
composer require workerman/workerman ~v5.1
) - webman-framework >= 2.1 (
composer require workerman/webman-framework ~v2.1
) - The
swoole
orswow
extension must be installed, or installcomposer require revolt/event-loop
(Fiber) - Coroutines are disabled by default and need to be enabled by setting the eventLoop.
Enabling Method
Webman supports enabling different drivers for different processes, so you can configure the coroutine driver through eventLoop
in config/process.php
:
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // Default is empty, automatically selects Select or Event, coroutines are not enabled
'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,
// To enable coroutines, set to Workerman\Events\Swoole::class or Workerman\Events\Swow::class or Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
]
// ... other configurations omitted ...
];
Tip
Webman can set differenteventLoop
for different processes, which means you can selectively enable coroutines for specific processes.
For example, in the configuration above, the service on port 8787 does not enable coroutines, while the service on port 8686 has coroutines enabled. This allows for mixed deployment of coroutines and non-coroutines when combined with Nginx forwarding.
Coroutine Example
<?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');
}
}
When eventLoop
is Swoole
, Swow
, or Fiber
, Webman creates a coroutine for each request to execute, allowing new coroutines to be created while processing the request.
Coroutine Limitations
- When using Swoole or Swow as the driver, coroutines will automatically switch on blocked IO, allowing synchronous code to execute asynchronously.
- When using the Fiber driver, coroutines will not switch on blocked IO, causing the process to enter a blocked state.
- When using coroutines, multiple coroutines cannot operate on the same resource simultaneously, such as database connections or file operations, which may lead to resource contention. The correct approach is to use a connection pool or locks to protect resources.
- When using coroutines, request-related state data should not be stored in global variables or static variables, as this may lead to global data pollution. The correct approach is to use coroutine context
context
to store and access them.
Other Considerations
Swow automatically hooks PHP's blocking functions at the bottom layer, but this hooking may affect some default behaviors of PHP, which can lead to bugs if you have Swow installed but are not using it.
So it is recommended:
- Do not install the Swow extension if your project is not using it.
- If your project uses Swow, set
eventLoop
toWorkerman\Events\Swow::class
.
Coroutine Context
In the coroutine environment, it is prohibited to store request-related state information in global variables or static variables, as this may lead to global variable pollution. For example:
<?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;
}
}
Note
In the coroutine environment, it is not forbidden to use global variables or static variables, but it is prohibited to use them to store request-related state data.
For example, global configurations, database connections, singletons of some classes, and other data that needs to be shared globally are recommended to be stored using global variables or static variables.
When the number of processes is set to 1, and we make two consecutive requests:
http://127.0.0.1:8787/test?name=lilei
http://127.0.0.1:8787/test?name=hanmeimei
We expect the results of the two requests to be lilei
and hanmeimei
, respectively, but the actual return value is both hanmeimei
.
This is because the second request overwrites the static variable $name
, and by the time the first request finishes sleeping and returns, the static variable $name
has already become hanmeimei
.
The correct approach should be to use context to store request state data.
<?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');
}
}
The support\Context
class is used to store coroutine context data. When the coroutine execution ends, the corresponding context data is automatically deleted.
In the coroutine environment, since each request is a separate coroutine, the context data will be automatically destroyed when the request is completed.
In a non-coroutine environment, the context will be automatically destroyed at the end of the request.
Local variables will not cause data pollution.
<?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;
}
}
Because $name
is a local variable, coroutines cannot access local variables of each other, so using local variables is safe for coroutines.
Locker
Sometimes some components or business logic may not consider the coroutine environment, leading to resource contention or atomicity issues. In this case, you can use Workerman\Locker
to lock and achieve queued processing to prevent concurrency issues.
<?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);
}
// Without adding locks, it may trigger errors like "Socket#10 has already been bound to another coroutine#10" in Swoole
// May trigger coredump in Swow
// No issues in Fiber as the Redis extension is synchronous blocking IO
Locker::lock('redis');
$time = $redis->time();
Locker::unlock('redis');
return json($time);
}
}
Parallel Execution
When we need to execute multiple tasks concurrently and obtain results, we can use Workerman\Parallel
to achieve this.
<?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) {
// Do something
return $i;
});
}
$results = $parallel->wait();
return json($results); // Response: [1,2,3,4]
}
}
Pool Connection Pool
Multiple coroutines sharing the same connection can lead to data confusion, so connection pools should be used to manage database, Redis, and other connection resources.
Webman has already provided components like webman/database, webman/redis, webman/cache, webman/think-orm, and webman/think-cache, all of which integrate connection pools and support usage in both coroutine and non-coroutine environments.
If you want to modify a component that does not have a connection pool, you can use Workerman\Pool
to implement it. Refer to the following code.
Database Component
<?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();
}
// Get connection from coroutine context to ensure the same coroutine uses the same connection
$pdo = Context::get('pdo');
if (!$pdo) {
// Get connection from the connection pool
$pdo = self::$pool->get();
Context::set('pdo', $pdo);
// When the coroutine ends, automatically return the connection
Coroutine::defer(function () use ($pdo) {
self::$pool->put($pdo);
});
}
return call_user_func_array([$pdo, $name], $arguments);
}
private static function initializePool(): void
{
// Create a connection pool with a maximum connection count of 10
self::$pool = new Pool(10);
// Set connection creator (for simplicity, omitted configuration file reading)
self::$pool->setConnectionCreator(function () {
return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
});
// Set connection closer
self::$pool->setConnectionCloser(function ($pdo) {
$pdo = null;
});
// Set heartbeat checker
self::$pool->setHeartbeatChecker(function ($pdo) {
$pdo->query('SELECT 1');
});
}
}
Usage
<?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"}]
}
}
More Coroutine and Related Components Introduction
Refer to the Workerman Coroutine Documentation
Mixed Deployment of Coroutines and Non-Coroutines
Webman supports mixed deployment of coroutines and non-coroutines, for example, handling ordinary business with non-coroutines and slow IO business with coroutines, by forwarding requests to different services through Nginx.
For example, in config/process.php
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // Default is empty, automatically selects Select or Event, coroutines are not enabled
'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,
// To enable coroutines, set to Workerman\Events\Swoole::class or Workerman\Events\Swow::class or Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
// ... other configurations omitted ...
];
Then use the Nginx configuration to forward requests to different services
upstream webman {
server 127.0.0.1:8787;
keepalive 10240;
}
# Add a new upstream for 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;
# Requests starting with /tast go to port 8686, replace /tast with your actual prefix if necessary
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;
}
# Other requests go to the original 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;
}
}
}