Eliminating Blocking Degradation in Webman/Workerman with a Coroutine Plugin
This article explains the blocking‑degradation problem in Webman/Workerman's event‑loop, analyzes why traditional solutions fall short, and introduces the webman‑coroutine plugin that adapts multiple coroutine drivers, rewrites the web server, and provides practical guidance for safe coroutine integration.
Introduction
During a busy period I finally had time to read the community discussion about coroutine support and the blocking‑degradation issue in Webman/Workerman. What started as a brief note turned into a full‑blown plugin, webman‑coroutine , after several iterations.
Current Situation
Blocking degradation in Workerman/Webman
Workerman uses a master/worker multi‑process model where each worker runs a single event‑loop handling streams, timers, and other events. The loop is single‑threaded, so if a callback blocks, the entire loop stalls until the callback returns. This creates a queue‑like situation: the event‑loop is the sole consumer, and a blocked callback prevents new events from being processed, eventually exhausting the worker pool.
In practice, long‑running or blocking calls (e.g., PDO, curl, file I/O) can fill workers, reducing throughput. The naive fix is to increase the number of workers, but this only mitigates the symptom.
Workerman’s Swoole driver does not use coroutines
Some assume that switching the driver to Swoole automatically enables coroutines. The actual implementation loads Swoole’s event‑loop but does not wrap callbacks in \Co\run(), so blocking I/O still blocks the loop. The underlying stream_socket_server() call waits for callbacks, and even with Swoole’s system‑call hooks, the main socket’s next event cannot be processed until the current callback finishes.
Consequences
Unintentional blocking occupies all workers, drastically lowering throughput.
PDO
curl
File read/write
Other blocking I/O
Traditional solution: increase the number of workers.
Long‑polling interfaces
HTTP‑SSE
Long‑connection scenarios
Timers with blocking business logic
Queue producer/consumer patterns
Traditional solution: custom processes or external services.
Solution
I developed a coroutine infrastructure plugin compatible with both Webman and Workerman, named webman‑coroutine . The plugin uses the adapter and factory patterns to abstract over common coroutine drivers (Swow, Swoole, php‑fiber/ripple), providing a unified API that works both in coroutine and non‑coroutine environments.
The plugin re‑implements the web server for Webman, enabling full coroutine support without invasive changes. Below is a simplified excerpt of the driver implementation for Workerman 4.x:
<?php
/**
* This file is part of workerman.
* Licensed under the MIT License
*/
namespace Workerman\Events;
use Workerman\Worker;
use Swoole\Event;
use Swoole\Timer;
class Swoole implements EventInterface {
protected $_timer = [];
protected $_timerOnceMap = [];
protected $mapId = 0;
protected $_fd = [];
public static $signalDispatchInterval = 500;
protected $_hasSignal = false;
public function add($fd, $flag, $func, $args = []) {
switch ($flag) {
case self::EV_SIGNAL:
$res = pcntl_signal($fd, $func, false);
if (! $this->_hasSignal && $res) {
Timer::tick(static::$signalDispatchInterval, function () { pcntl_signal_dispatch(); });
$this->_hasSignal = true;
}
return $res;
case self::EV_TIMER:
case self::EV_TIMER_ONCE:
$timer_id = $this->mapId++;
$t = (int)($fd * 1000);
if ($t < 1) $t = 1;
$timer_id = Timer::tick($t, function () use ($func, $args, $timer_id) {
try { call_user_func_array($func, (array)$args); }
catch (\Exception $e) { Worker::stopAll(250, $e); }
catch (\Error $e) { Worker::stopAll(250, $e); }
});
return $timer_id;
// ... other cases omitted for brevity ...
}
}
// del, clearAllTimer, loop, destroy, getTimerCount implementations omitted
}
?>The plugin registers the coroutine‑enabled web server by extending App and overriding lifecycle methods such as onWorkerStart, onConnect, onMessage, and onClose. It tracks per‑connection coroutine counts, uses a WaitGroup to limit concurrent coroutines, and safely recycles resources.
<?php
class CoroutineWebServer extends App {
protected static array $_connectionCoroutineCount = [];
public static function getConnectionCoroutineCount(?string $id = null) {
return $id === null ? static::$_connectionCoroutineCount : (static::$_connectionCoroutineCount[$id] ?? 0);
}
public static function unsetConnectionCoroutineCount(string $id, bool $force = false) {
if (! $force && self::getConnectionCoroutineCount($id) > 0) return;
unset(static::$_connectionCoroutineCount[$id]);
}
public function onMessage($connection, $request, ...$params) {
$connectionId = spl_object_hash($connection);
$waitGroup = new WaitGroup();
$waitGroup->add();
new Coroutine(function () use (&$res, $waitGroup, $params, $connectionId) {
$res = parent::onMessage(...$params);
self::$_connectionCoroutineCount[$connectionId]--;
self::unsetConnectionCoroutineCount($connectionId);
$waitGroup->done();
});
self::$_connectionCoroutineCount[$connectionId] = (self::$_connectionCoroutineCount[$connectionId] ?? 0) + 1;
$waitGroup->wait();
return $res;
}
// other lifecycle methods omitted for brevity
}
?>During testing with Workerman 5.x I discovered several bugs in the original Swoole driver and submitted a PR (e.g., "fix: all coroutines must be canceled before Event::exit #1059"). The plugin also works in pure Workerman environments.
Practical Experience
1. Coroutines are not a silver bullet
Coroutines do not shorten inherently time‑consuming logic; they merely allow other tasks to run during blocking periods, effectively trading space for time.
2. Memory model in PHP
Arrays and objects reside on the heap, while scalars live on the stack. Coroutine switches save registers and stack frames but not heap data, leading to race conditions when multiple coroutines share the same heap objects.
$a = new \stdClass();
$a->id = 1;
new Coroutine(function () use ($a) { $a->id = 2; });
new Coroutine(function () use ($a) { $a->id = 3; });
// Final value of $a->id is nondeterministic
echo $a->id;Similar race conditions exist for stack variables passed by reference.
$a = 1;
new Coroutine(function () use (&$a) { $a = 2; });
new Coroutine(function () use (&$a) { $a = 3; });
// Final value of $a is nondeterministic
echo $a;Heap data can be cloned to avoid sharing, but resources cannot be cloned. A common pattern is to store coroutine‑local context in a static array keyed by coroutine ID.
3. Database connection pooling
PDO uses blocking I/O; when multiple coroutines share a single PDO connection, their queries are serialized, and results may be delivered to the wrong coroutine, causing data corruption. Solutions include per‑coroutine connections, a connection pool, or using coroutine‑aware database libraries such as hyperf/database or hyperf/db.
4. Other components that need pooling
Any object or array that is shared across coroutines (e.g., caches, HTTP clients) suffers the same race‑condition problem and should be either isolated per coroutine or managed via a pool.
5. Ongoing work
The current plugin mainly solves blocking degradation for long‑polling, non‑blocking timers, queue processing, and worker/server coroutineification. Future work includes non‑intrusive database connection pooling for Webman, coroutine‑friendly components for Workerman, and a unified API for various coroutine drivers (Swow, Swoole, Ripple, Revolt).
Contributions via issues and pull requests are welcome. If any part of this article contains inaccuracies, please point them out.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Tech Hub
Sharing cutting-edge internet technologies and practical AI resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
