To correct a mistake in previous email, there was an error in the second code 
snippet of my last email. The correct code is as follows:


```php
<?php
Co\run(function() {
&nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; $fp = stream_socket_server("tcp://0.0.0.0:8000", 
$errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
&nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $conn = stream_socket_accept($fp);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; Co\go(function() use ($conn) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 
fwrite($conn, 'The local time is ' . date('n/j/Y g:i a'));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sleep(1);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });


&nbsp; &nbsp; $n = 2000;
&nbsp; &nbsp; while($n--) {
&nbsp; &nbsp; &nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $fp = 
stream_socket_client("tcp://127.0.0.1:8000", $errno, $errstr, 30);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; echo fread($fp, 8192), 
PHP_EOL;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; }
});
```


The logic here is to start a server coroutine, which then accepts client 
connections. Each new connection spawns a coroutine that sends the current time 
string to the client every second.


The code below directly creates 2,000 clients in the current process, connects 
them to the server, and reads data. Note: If you want to test this code, I 
recommend lowering the number of concurrent connections, otherwise your 
terminal may freeze or become unresponsive.


This program can accommodate virtually any PHP function, such as mysqli, pdo, 
redis, curl, and more—all of which can be executed concurrently with ease.


Notice that only two new APIs have been introduced in the code: `Co\run` and 
`Co\go`; the rest are standard or commonly used PHP extension functions.


This is precisely the strength of Swoole, and the focus of the True-Async 
team’s ongoing work.


Hopefully, the PHP language will one day include such powerful features 
natively.



----------
Tianfeng Han


&nbsp;
------------------&nbsp;Original&nbsp;------------------
From: &nbsp;"韩天峰"<[email protected]&gt;;
Date: &nbsp;Fri, Nov 21, 2025 07:03 PM
To: &nbsp;"Edmond Dantes"<[email protected]&gt;; "Rowan Tommins 
[IMSoP]"<[email protected]&gt;; 
Cc: &nbsp;"php internals"<[email protected]&gt;; 
Subject: &nbsp;Re: [PHP-DEV] [VOTE] True Async RFC 1.6

&nbsp;

My thanks to Edmond for his work on PHP async I/O. Regardless of how the vote 
turns out, I believe everything is moving in a positive direction. This will 
undoubtedly leave a bold mark in the history of PHP’s evolution.


I’ll share some information and thoughts to help everyone understand async. 
These may include views on PHP Fiber, amphp, reactphp, and FrankenPHP, but 
please remember they are purely technical reflections, with no praise or 
criticism implied.


1. What lies at the core of Swoole’s async design
Using Boost.Context assembly to implement C/C++ stackful coroutines is no 
longer esoteric; PHP Fiber and Swoole are almost identical in their low-level 
principles. The only difference is that Swoole suspends and resumes coroutines 
entirely in C/C++, whereas PHP Fiber does the opposite—suspension happens in 
PHP code. While PHP also exposes relevant APIs, they are rarely used in Swoole.


Because both the C stack and the PHP stack are fully preserved, this approach 
is actually very safe and won’t cause memory errors—unless static or global 
memory is misused. Swoole runs over 1,700 tests on GitHub Actions, many of 
which involve multiple coroutines issuing concurrent requests. Before testing, 
containers spin up mysql, pgsql, oracle, redis, firebirdsql, httpbin, 
tinyproxy, pure-ftpd, and many other databases and servers to interact with 
code in phpt files. The breadth of these tests speaks to its reliability.


Unlike amphp/reactphp, Swoole does not invent new APIs; it reuses PHP’s 
existing functions. Swoole hooks into PHP streams, the standard library, and 
other extension functions—such as sleep, stream_socket_client, 
stream_socket_server, file_get_contents, fsockopen, curl_*, mysqli, pdo_mysql. 
Inside a Swoole coroutine, these calls are no longer synchronous blocking I/O; 
they become non-blocking. When I/O isn’t ready, the runtime suspends the 
current coroutine and uses epoll to watch for readable events, resuming the 
coroutine only when the operation completes.


An example:


Co\run(function() {
&nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sleep(1);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $fp = 
stream_socket_client("tcp://127.0.0.1:8000", $errno, $errstr, 30);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; echo fread($fp, 8192), PHP_EOL;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });


&nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; $fp = stream_socket_server("tcp://0.0.0.0:8000", 
$errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN);
&nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $conn = stream_socket_accept($fp);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwrite($conn, 'The local time is ' . 
date('n/j/Y g:i a'));
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });


&nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; $redis = new Redis();
&nbsp; &nbsp; &nbsp; &nbsp; $redis-&gt;connect('127.0.0.1', 6379);
&nbsp; &nbsp; &nbsp; &nbsp; while(true) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $redis-&gt;subscribe(['test'], 
function ($instance, $channelName, $message) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; echo 'New redis 
message: '.$channelName, "==&gt;", $message, PHP_EOL;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });


&nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; $redis = new Redis();
&nbsp; &nbsp; &nbsp; &nbsp; $redis-&gt;connect('127.0.0.1', 6379);
&nbsp; &nbsp; &nbsp; &nbsp; $count = 0;
&nbsp; &nbsp; &nbsp; &nbsp; while(true) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sleep(2);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $redis-&gt;publish('test','hello, 
world, count='.$count++);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });
});


By conventional understanding, this code shouldn’t run: every function that 
performs network I/O would block the entire process. But in the Swoole 
environment, the program runs smoothly. We can even modify the code to increase 
the number of clients by several thousand, and it still runs stably.


Co\run(function() {
&nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; sleep(1);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $fp = 
stream_socket_client("tcp://127.0.0.1:8000", $errno, $errstr, 30);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; echo fread($fp, 8192), PHP_EOL;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; });


&nbsp; &nbsp; $n = 2000;
&nbsp; &nbsp; while($n--) {
&nbsp; &nbsp; &nbsp; &nbsp; Co\go(function() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $fp = 
stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr, STREAM_SERVER_BIND 
| STREAM_SERVER_LISTEN);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; while(1) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; $conn = 
stream_socket_accept($fp);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; fwrite($conn, 'The 
local time is ' . date('n/j/Y g:i a'));
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; });
&nbsp; &nbsp; }
});


Swoole’s aim is to leverage PHP’s existing ecosystem rather than build a new 
one. If we were starting from scratch—discarding PHP’s commonly used functions 
and learning an entirely new async API—why wouldn’t developers simply switch 
languages?


Now that true-async has adopted Swoole’s approach, I think that’s an excellent 
choice.


2. Where PHP-FPM falls short


If all you do is read/write MySQL and generate HTML, PHP-FPM is already superb. 
If I’m building a web project that only depends on MySQL, I wouldn’t use 
Swoole; PHP-FPM is the best choice. But many modern web projects need to call 
external HTTP APIs, and slow requests often render PHP-FPM unavailable, which 
is frustrating. Async exists precisely to address this. With the rise of 
ChatGPT, streaming responses such as SSE and full-duplex communication via 
WebSocket will become increasingly common—technologies that PHP-FPM doesn’t 
support well. Many developers choose Node.js or Go instead. The influence of 
Swoole or amphp remains limited; only a small subset of developers opt to stay 
with PHP for async programming using these solutions.


If PHP can adopt true-async or other AsyncIO solutions and provide support for 
async I/O at the language level, it would be tremendous news for PHP users. In 
essence, async I/O is a runtime matter—much like Node.js in relation to V8. New 
PHP syntax isn’t required; Swoole, for instance, adds no new syntax—just some 
functions and classes—just as fastcgi_finish_request and fpm_get_status are 
php-fpm–only functions.


3. FrankenPHP
FrankenPHP is a wonderful project that uses Go to give PHP additional 
capabilities, with great room for exploration.


In an RFC for a Polling API, author Jakub Zelenka—also a FrankenPHP 
maintainer—shared a technical idea: consider implementing a goroutine version 
of the TSRM thread isolation scheme. Each goroutine would have its own Zend VM 
environment—essentially a goroutine-based php-fpm.


I believe this approach may pose significant challenges, especially regarding 
memory resources.


Today, when running Symfony or Laravel under PHP-FPM with 100–200 worker 
processes, memory pressure is already heavy. If each process consumes tens to 
over a hundred megabytes, the group can easily use up to 20 GB. With 
goroutines, if you launch thousands or tens of thousands to handle requests 
concurrently, memory usage could become enormous.


By contrast, coroutines are designed to be very lightweight: a suspended 
coroutine should retain only the call stack and a small amount of 
request/session-related memory, while other resources can be shared and reused 
across requests. This drastically reduces memory usage while still allowing a 
large number of simultaneous requests. When a request is slow, suspension 
incurs little cost.


4. Fiber
If Fiber and coroutines coexist as execution units, I agree it can be 
confusing. But the current Fiber simply can’t be used in a Swoole-like runtime 
with extensive low-level switching.


Although Fiber landed in PHP 8.1, Swoole cannot use any Fiber APIs.


In addition, Fiber doesn’t fully switch all global memory state—for example 
OG(handlers), BG(serialize), BG(unserialize)—so it’s unclear whether issues 
exist there.


5. Golang’s abundance of synchronization primitives


Go’s goroutine isn’t purely a coroutine; it’s a combination of thread and 
coroutine, which necessitates many locks, mutexes, semaphores, and atomics to 
resolve data races. PHP does not support multithreading. Whether it’s Fiber, 
Swoole, or any other coroutine implementation in PHP, execution is 
single-threaded: only one coroutine runs at a time, and until it yields, no 
other coroutine runs.


Therefore, PHP coroutines are not a complex concept but a clear and 
straightforward one.


If the true-async RFC vote doesn’t pass this time, I think we can split the 
work into several parts, aiming for each RFC to accomplish just one thing.


I think the most important task now is to allow coroutine switching in 
low-level C code, not just in PHP code. Whether we call it coroutines or Fiber 
2.0 is fine. On top of that, other work can be introduced via future RFCs to 
progressively strengthen the design.


Lastly, I sincerely hope PHP keeps getting better. Thanks all for your 
contributions. Open discussion is always beneficial.





------
Tianfeng Han

&nbsp;


------------------ Original ------------------
From: &nbsp;"Edmond Dantes"<[email protected]&gt;;
Date: &nbsp;Fri, Nov 21, 2025 03:17 PM
To: &nbsp;"Rowan Tommins [IMSoP]"<[email protected]&gt;; 
Cc: &nbsp;"php internals"<[email protected]&gt;; 
Subject: &nbsp;Re: [PHP-DEV] [VOTE] True Async RFC 1.6

&nbsp;

Hello.

Imagine that we have an application like this.

```php
class AuthService
{
&nbsp; &nbsp; private static ?self $instance = null;

&nbsp; &nbsp; private PDO $db;
&nbsp; &nbsp; private ?string $sessionId = null;

&nbsp; &nbsp; // Private constructor for singleton
&nbsp; &nbsp; private function __construct(PDO $db)
&nbsp; &nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $this-&gt;db = $db;
&nbsp; &nbsp; }

&nbsp; &nbsp; // Get singleton instance
&nbsp; &nbsp; public static function getInstance(PDO $db): self
&nbsp; &nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; if (self::$instance === null) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; self::$instance = 
new self($db);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; return self::$instance;
&nbsp; &nbsp; }

&nbsp; &nbsp; public function login(string $email, string $password): bool
&nbsp; &nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; // Find user by email
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $stmt = $this-&gt;db-&gt;prepare('SELECT 
* FROM users WHERE email = ?');
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $stmt-&gt;execute([$email]);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $user = $stmt-&gt;fetch(PDO::FETCH_ASSOC);

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; // Invalid credentials
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; if (!$user || !password_verify($password, 
$user['password_hash'])) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; return false;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; }

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; // Generate and save session ID
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $this-&gt;sessionId = 
bin2hex(random_bytes(16));

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $stmt = $this-&gt;db-&gt;prepare(
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; 'INSERT INTO 
sessions (user_id, session_id) VALUES (?, ?)'
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; );
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $stmt-&gt;execute([$user['id'], 
$this-&gt;sessionId]);

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; return true;
&nbsp; &nbsp; }

&nbsp; &nbsp; // Return current session ID
&nbsp; &nbsp; public function getSessionId(): ?string
&nbsp; &nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; return $this-&gt;sessionId;
&nbsp; &nbsp; }
}
```

One day you decide you want more performance and make a single PHP
process handle multiple connections concurrently. You wrap each
request in a separate coroutine and try to use the old code.

```php
$server = new Swoole\Http\Server("127.0.0.1", 9501);

$server-&gt;on("request", function ($req, $res) {

&nbsp; &nbsp; // create DB connection (just for example)
&nbsp; &nbsp; $db = new PDO('mysql:host=localhost;dbname=test', 'root', '');

&nbsp; &nbsp; // get singleton
&nbsp; &nbsp; $auth = AuthService::getInstance($db);

&nbsp; &nbsp; // read request data
&nbsp; &nbsp; $data = json_decode($req-&gt;rawContent(), true);

&nbsp; &nbsp; $email = $data['email'] ?? '';
&nbsp; &nbsp; $password = $data['password'] ?? '';

&nbsp; &nbsp; // call old sync code
&nbsp; &nbsp; $ok = $auth-&gt;login($email, $password);

&nbsp; &nbsp; if ($ok) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $res-&gt;end("Logged in, session: " . 
$auth-&gt;getSessionId());
&nbsp; &nbsp; } else {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $res-&gt;status(401);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; $res-&gt;end("Invalid credentials");
&nbsp; &nbsp; }
});

$server-&gt;start();
```

What is happening here?
Now, in PHP, inside a single process or thread, the same code is
literally handling multiple connections.
At the same time, there are constant switches between different
requests at the points where MySQL queries occur.

That is, when the code executes
$stmt-&gt;execute([$email]);
control is passed to another coroutine with a different
$stmt-&gt;execute([$email]);

What breaks in this code?
Correct, coroutines break the singleton because they alternate writing
different Session IDs!

And what does not change in this code?
The SQL queries can remain unchanged.

The first problem with shared memory between coroutines can ONLY be
solved by the programmer. Only the programmer. There is no solution
that would make this happen automatically.
Yesterday we talked about how we can help the programmer detect such
situations during debugging. But in any case, only the programmer
**CAN** and **MUST** solve this problem.

The difference is that you don’t need to rewrite everything else.
The focus is only on the issue of concurrent access to memory.

The essence of the choice is how much code needs to be rewritten.
Almost everything, or only the code with global state.
My choice is: it’s better to rewrite only the code with global state —
or switch to Go and avoid the pain :)

As for the rest, I will write a separate message so as not to clutter things up.

----
Edmond

Reply via email to