Re: [VOTE] True Async RFC 1.6

From: Date: Fri, 21 Nov 2025 11:30:18 +0000
Subject: Re: [VOTE] True Async RFC 1.6
References: 1 2 3 4 5 6 7 8 9  Groups: php.internals 
Request: Send a blank email to internals+get-129359@lists.php.net to get a copy of this message
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;"韩天峰"<rango@swoole.com&gt;;
Date: &nbsp;Fri, Nov 21, 2025 07:03 PM
To: &nbsp;"Edmond Dantes"<edmond.ht@gmail.com&gt;; "Rowan Tommins
[IMSoP]"<imsop.php@rwec.co.uk&gt;; 
Cc: &nbsp;"php internals"<internals@lists.php.net&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"<edmond.ht@gmail.com&gt;;
Date: &nbsp;Fri, Nov 21, 2025 03:17 PM
To: &nbsp;"Rowan Tommins [IMSoP]"<imsop.php@rwec.co.uk&gt;; 
Cc: &nbsp;"php internals"<internals@lists.php.net&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


Thread (106 messages)

« previous php.internals (#129359) next »