Coroutines in Swoole

Introduction

In this post I will go through some details about the coroutines in Swoole (v4.0+). The first sections will explain the basics and certain details and then examples will be given. Numbers in [] refer to entries in the Notes section, while numbers in () refer to the Examples section. There are many links in the post pointing to the original documentation of Swoole in Chinese – please use Google Translate for these. It does a good job when translating from Chinese to English. The code in this post refers to Swoole version 4.4.6 or higher.

Swoole coroutines (1) can be used as part of the Swoole Server (2) inside the some of the server handlers (onRequest, onConnect…) or without it. The coroutines are modeled after their implementation in Go.

When the code is running in a handler defined for an event on Swoole Server this code will be already running in a coroutine. This is enabled by default but can be disabled with $server->set([‘enable_coroutine’ => false]); . Given this, all examples provided in this post assume that are already running inside a coroutine unless otherwise noted.

The Basics

The coroutine in Swoole can be understood as a thread running in user mode. This means that no process/OS thread switch occurs on coroutine switch. Only the local stack is switched and the global space (heap) remains the same. This makes the switch less costly. For every coroutine Swoole initially allocates just 8kb – this is to be used for the coroutine stack.

Each coroutine has its own stack but shares the global state of the PHP process (the Worker) with the rest of the coroutines. This includes the heap and any resources. Of course, the objects in the heap will be accessible only by the coroutines which have a local pointer to these in their stack. This is different for the static variables (be these local to a function or to a class) – these are accessible at any point by any coroutine (subject to the visibility rules of course). And it is important to mention that the resources are also shared between all coroutines. Like any other local variable, these will be shared only if they are explicitly provided to the main function of the coroutine or accessed through a static variable. This will allow for two coroutines to use the same database connection which will trigger an error. The first coroutine would have sent a query over the connection and will yield to the second coroutine while waiting for the results. The second coroutine (being unaware of that) will push another query to the connection before the data from the previous one to have been received.

Swoole coroutines explained
Representation of the coroutines running within a single Worker
  • The green bars represent the individual coroutine stacks with the little green “bricks” the individual stack frames
  • the yellow bricks represent the stack frame that yielded the control to the scheduler
  • the red brick represents the stack frame that is currently being executed
  • the red outline represents what the current coroutine has access to. It is completely isolated from the other coroutines (unless an object or variable by reference was passed in between the coroutines)
  • the heap is represented in blue and is accessible by all coroutines (when the Worker switches to each of them). Each of the variables in the heap is subject to the visibility rules and if these allow access the coroutines access a shared global space.
  • the black lines represent parent/child relation between the coroutines. Most of the coroutines in the given example are started by the Worker but there are two (in pink) that are started by another coroutine.
  • The parent/child relationship of the coroutines does not mean that that they are nested like the function calls/stack. Unlike the functions the child coroutine can end after the parent one and both coroutines are running in parallel (in terms that they can get and yield execution multiple times in between giving the impression of parallel execution, not that they are actually running in parallel as it is explained further down). This is illustrated in the following image:
Swoole Corotuines execution example
Coroutine creation and execution

As it can be seen from the image above the first coroutine (red one) is started by the Worker (Swoole Server to be precise). The second one (green) too. The orange one (third) is started by the first coroutine and ends before the parent. Unlike the dark blue one (6th) which is started by the 5th coroutine but ends after it.

The second graph shows the number of coroutines running simultaneously. It is important to note that this does not represent the stacks of the coroutines but just the number of coroutines. The coroutines can have stacks of variable depth and are completely independent of each other.

The Details

It is very important to note that the core PHP libraries and DB extensions are blocking and do not allow for coroutine switching. This means that if you execute mysqli_connect() the Swoole Worker will wait (block) for the result of the operations instead of yielding the control to another coroutine. Thus running an existing PHP code in Swoole will make no use at all of the coroutines.

Only the modules found under Swoole\Coroutine (like Swoole\Coroutine\Mysql) and functions like Swoole\Coroutine\sleep() (NOTE: these are being moved to Swoole\Coroutine\System in v4.4.6) provided by Swoole will allow for coroutine switching. Another way is to use the Swoole\Runtime::enableCoroutine() that that will enable coroutine support for some core PHP functionality (file operations, the Redis extension, stream functions, soap). This means that \sleep() is blocking (the Worker will stop executing and wait without switching to another coroutine) while Swoole\Coroutine\System::sleep() is not blocking and will yield the control.

The coroutines are switched on cooperative basis, not preemptive[1]. The coroutines yield the control to the scheduler only at certain points. In Swoole these are requests that can be served asynchronously[2] like database queries, file operations, etc. By saying asynchronous I do not mean that these are using callbacks, but instead that at the point of a database query being sent the coroutine gives the control back to the scheduler to run another coroutine. The first coroutine will be resumed once the data is received from the DB and the currently running coroutine gives up the control.

Having the switching on a cooperative basis makes the userland code much safer. This allows for writing in static or global variables without locking as there is no possibility for another coroutine to get in the middle.

Another important detail in Swoole coroutines implementation is that the coroutines are not actually independent threads executing simultaneously. The Swoole Worker executes only a single coroutine at any given time. The rest are either waiting for data or (when the data is received) for the current coroutine to yield. This means that at any given point in time only a single coroutine is being executed by the worker. This is to say that if a coroutine is started for the purpose of offloading an expensive calculation from the main coroutine this will achieve nothing. Once the coroutine is started it will block the worker (and the parent coroutine) until the calculation is done.

Side Effects and others

Having cooperative multitasking and only one coroutine being executed by the Worker allows for a single coroutine that enters an endless loop to block the worker / starve the rest of the coroutines. In the traditional PHP setup where a single PHP process serves a single request, this will prevent only the current request to be served but in Swoole Server context this means that any requests that are currently being served by the worker will fail as their respective coroutines will not get CPU time.

Another side effect of the cooperative switching is that it is possible only one coroutine to be executed at any given time if the code of the coroutine does not invoke any code allowing for context switch (DB function, fs, co::sleep() etc) or doesn’t start explicitly a new coroutine with go()/Coroutine::create(). No matter how many requests are coming only a single coroutine will be executed if no points of yield exist in the code serving the requests. This combined with the persistent nature of Swoole (the memory is preserved between the requests) allows for caching in memory without any access to an external system be it Redis, Memcached or APCU. Without such access yielding to other coroutines just doesn’t occur. (Offtopic) Besides the theoretically possible effect of having just single coroutine running at any given time in the Worker, this allows for huge speed improvement as all data/objects can reside in the PHP memory and no external system is accessed and no unserialization of the data is performed.

Having the resources within a worker shared between the coroutines requires the use of multiple connections (each serving a different coroutine). This though does not represent an overhead compared to the traditional setup where a connection is opened at the beginning of each request. On the contrary – the global shared state allows for a pool of database connections to be created and connections from this pool to be obtained by the coroutines as needed and then released. Because of the persistent nature of Swoole, these connections do not have to be opened and closed for each request but instead once they are opened when the Swoole Server is started these only need to be pinged to keep them alive. The connection pool provides a speedup compared to the traditional setup as it saves time for opening the DB connection.

Additional Features – channels, deferring

For the purpose of communication between the coroutines, Swoole provides the Swoole\Coroutine\Channel. When a channel is shared between coroutines (7) this allows the coroutines to push() data to the channel and in the same time the master coroutine (the Channel can be used only in Coroutine context) uses a blocking pop() to get the data out. This means that if we are going to have to sub Coroutines collecting some data then the channel needs to be of size 2 and the master coroutine needs to contain two pop() calls. It is important to note that there is no guarantee in which order the data will be pushed to the channel meaning that at the master coroutine when popping the results the first result is not guaranteed to be the first coroutine that was started as there is IO operation of undefined time.

Additional functionality that is provided is a callback that can be queued to be executed at the time the coroutine is exited (9). This is done with defer() which is an alias of Swoole\Coroutine::defer(). This can be used for cleanup, resource deallocation etc. Multiple callbacks can be queued. These will be executed in the reverse order (last queued first executed) as this is the proper way of resource deallocation.

Examples

(1) A basic example of creating a coroutine (outside Swoole Server) is:

<?php
Swoole\Coroutine::create(function(){
    print 'im in coroutine'.PHP_EOL;
});
print 'im outside coroutine'.PHP_EOL;

//alternative syntax
//go() is alias of Swoole\Coroutine::create()
go(function(){
});

//and with provided arguments
go(function($arg1, $arg2){
    print $arg1.PHP_EOL;
    print $arg2.PHP_EOL;
}, 55, 66);

//the signature of the method being:
/**
 * Creates a new coroutine and returns a unique ID (cid).
 * The coroutine execution starts immediately 
 * @param callable $func
 * @param array ...$params Optional arguments that will be passed to the callable. 
 * @return int A positive int containing the coroutine ID
 */
public static function create( callable $func, $params) { }

(2) Serving a request in Swoole Server (the request is served inside a coroutine):

<?php
$Server = new Swoole\Http\Server('0.0.0.0', 8081, SWOOLE_PROCESS);
$Server->on('request', function (\Swoole\Http\Request $SwooleRequest, \Swoole\Http\Response $SwooleResponse) {
    //this code is in coroutine context
    print Swoole\Coroutine::getCid();// 1 and incrementing with every request
    print Swoole\Coroutine::getPcid();// -1 there is no parent coroutine
    $SwooleResponse->end('ok');
});
$Server->start();

(3) Switching between coroutines:

<?php
go(function() {
    print 'co 1 started'.PHP_EOL;
    Swoole\Coroutine\System::sleep(1);//this yields
    //alternative syntax
    //co::sleep(1);// \co is alias of \Swoole\Coroutine
    print 'co 1 ended'.PHP_EOL;
} );
go(function() {
    print 'co 2 started'.PHP_EOL;
    co\System::sleep(1);//yields
    print 'co 2 ended'.PHP_EOL;
} );
//output being
//co 1 started
//co 2 started
//co 1 ended
//co 2 ended

(4) Example of blocking and non-blocking functions:

<?php
go(function(){
    print 'co 1 start'.PHP_EOL;

    //this will block the worker until the writing is finished
    //and will not yield to another coroutine
    file_put_contents('file.txt','some content');

    print 'co 1 after first file'.PHP_EOL;

    //this will yield and main scope will continue to execute
    //as the main scope starts another coroutine this coroutine will be immediately started
    Swoole\Coroutine\System::writeFile('file2.txt','another content');
    
    print 'co 1 after second file'.PHP_EOL;
});

go(function(){
    print 'co 2 start'.PHP_EOL;
});

//the output being
//co 1 start
//co 1 after first file
//co 2 start
//co 1 after second file

(5) Examples of sharing variables between coroutines. This can be done with a variable passed by reference or an object in use() or by using a static variable (local in the function or on a class):

<?php
$shared_var = 'initial value';

go(function() use (&amp;$shared_var) {
    
    print 'initial value: '.$shared_var.PHP_EOL;
    $shared_var = 'co 1 value';
    co::sleep(1);//yields
    print 'current value: '.$shared_var.PHP_EOL;//prints "co 2 value"
    
});

go(function() use (&amp;$shared_var){
    $shared_var = 'co 2 value';
});

(6) Modification of the shared variable must be done carefully to ensure that there is no code that yields in the critical section of the code:

<?php
$shared_var = 'initial value';

go(function() use (&amp;$shared_var) {
    
    print 'initial value: '.$shared_var.PHP_EOL;
    
    //start critical section
    if ($shared_var === 'initial value') {
        //it is wrong to have yielding code in a critical section
        //as the "co 1 value" must be set only if the value is the initial one
        $some_data = Swoole\Coroutine\System::readFile('some_file.txt');
        if ($some_data === 'expected value') {
            $shared_var = 'co 1 value';
        }
    }
    //end critical section
    
    print 'current value: '.$shared_var.PHP_EOL;//prints "co 1 value" which is not the wanted result
    
});

go(function() use (&amp;$shared_var){
    $shared_var = 'co 2 value';
});


//with the correct implementation following:

$shared_var = 'initial value';

go(function() use (&amp;$shared_var) {
    
    print 'initial value: '.$shared_var.PHP_EOL;
    
    //start critical section
    $some_data = Swoole\Coroutine\System::readFile('some_file.txt');
    if ($shared_var === 'initial value' &amp;&amp; $some_data === 'expected value') {
        //there can not be any yielding code between the value check and the value modification
        $shared_var = 'co 1 value';
    }
    //end critical section
    
    print 'current value: '.$shared_var.PHP_EOL;//prints "co 1 value" which is not the wanted result
    
});

go(function() use (&amp;$shared_var){
    $shared_var = 'co 2 value';
});

And of course, it is important to mention that if running in preemptive mode[1] the above code will still be wrong as a coroutine switch may occur just after the if() but before the value modification and overwriting of the value set by another coroutine can occur. To avoid this proper locking needs to be in place around the critical code section.

(7) Doing two tasks in parallel and collecting the result with Swoole\Coroutine\Channel. This Channel has a fixed size and is used to exchange data between coroutines. The data pushed to the Channel is not serialized which means objects & resources can be pushed without side effects. The Channel::pop() is blocking meaning the execution of the coroutine will stop until there is something pushed to the channel.

<?php
//we want to collect the results from the coroutines so we set the size of the channel to 2
go(function(){
    //the channel can be used only in coroutine context
    //because of this a parent coroutine is needed
    //if this is in the onRequest handler the code would already be in coroutine
    $channel = new Swoole\Coroutine\Channel(2);
    go(function() use ($channel) {
        $mysql = new Swoole\Coroutine\MySQL();
        $mysql->connect([
            'host' => '127.0.0.1',
            'user' => 'user',
            'password' => 'pass',
            'database' => 'testdb',
        ]);
        $data = $mysql->query('select * from menus');
        $channel->push($data);
    });

    go(function() use ($channel) {
        $mysql = new Swoole\Coroutine\MySQL();
        $mysql->connect([
            'host' => '127.0.0.1',
            'user' => 'user',
            'password' => 'pass',
            'database' => 'testdb',
        ]);
        $data = $mysql->query('select * from orders');
        $channel->push($data);
    });

    $data1 = $channel->pop();
    $data2 = $channel->pop();

    //there is no guarantee which result will come first!
    print_r($data1);
    print_r($data2);

});

Please note that in the above example there is no guarantee which result will be in which $data variable. $data1 does not necessarily hold the first query so additional care needs to be taken to ensure that the two results can be distinguished from each other (if they have the same columns/keys).

(8) The above result can also be achieved using the setDefer() and recv() methods on the Swoole\Coroutine\Mysql client (The Redis and Http\Client clients too have the setDefer() method). The setDefer() method allows without the explicit use of coroutines to allow the execution of the current scope to continue after the asynchronous request is sent.

<?php
//for the same of completeness the code is given with server example instead of explicit master coroutine
$Server = new Swoole\Http\Server('0.0.0.0', 8081, SWOOLE_PROCESS);
$Server->on('request', function (\Swoole\Http\Request $SwooleRequest, \Swoole\Http\Response $SwooleResponse) {
//go(function(){ //no need - we are in coroutine context already
    $mysql1 = new Swoole\Coroutine\MySQL();
    $mysql1->connect([
        'host' => '127.0.0.1',
        'user' => 'user',
        'password' => 'pass',
        'database' => 'testdb',
    ]);
    $mysql1->setDefer();
    $mysql1->query('select * from menus');

    $mysql2 = new Swoole\Coroutine\MySQL();
    $mysql2->connect([
        'host' => '127.0.0.1',
        'user' => 'user',
        'password' => 'pass',
        'database' => 'testdb',
    ]);
    $mysql2->setDefer();
    $mysql2->query('select * from orders');

    $data1 = $mysql1->recv();
    $data2 = $mysql2->recv();
    
    //in this case the order of the data is guaranteed - data1 holds the data from the first connection
    print_r($data1);
    print_r($data2);
    $SwooleResponse->end('ok');
//});
});
$Server->start();

(9) A basic example illustrating how defer() works:

<?php
go(function(){
    print 'coroutine start'.PHP_EOL;
    Swoole\Coroutine::defer(function(){
        print 'first deferred code'.PHP_EOL;//but last to be executed...
    });
    print 'coroutine executing ...'.PHP_EOL;
    //alternative syntax
    defer(function(){
        print 'second deferred code'.PHP_EOL;
    });
    print 'coroutine end'.PHP_EOL;
});

//the output being
//coroutine start
//coroutine executing ...
//coroutine end
//second deferred code
//first deferred code

Due to the nature of swoole – preserving the memory/context between the requests, it is vital to deallocate any allocated resources during execution. This is where defer() must be used. Immediately after a resource is allocated the deallocation code must be put.

<?php
$ConnectionPool = new ConnectionPool();
go(function() use ($ConnectionPool) {
    $Connection = $ConnectionPool->get_connection();
    Swoole\Coroutine::defer(function() use ($Connection) {
        $Connection->free();
    });
    $Lock = $Connection->obtain_lock();
    defer(function() use ($Lock) {
        $Lock->release();
    });
    //function body
});

Notes

  1. Swoole docs and examples also mention a preemptive mode by using Co::set([‘enable_preemptive_scheduler’ => true]) or with ini_set(“swoole.enable_preemptive_scheduler”,”1″). Using this mode though poses challenges when accessing the static or global/shared variables. To modify these and ensure that no other coroutine is modifying these at the same time locking needs to be employed.
  2. Swoole did have asynchronous classes as well but as of v4.3.0 these were removed from the main extension into a separate one (swoole/ext-async) as there is no need to have async functionality when there are coroutines available.

Additional reading

There are three very good articles (still readable with Google translate) on wiki.swoole.com about the coroutines:

Additional sources:

Find me at

Veselin Kenashkov

Developer at Azonmedia
Loves PHP, frameworks and old computers. Currently interested in Swoole and Vuejs.
Veselin Kenashkov
Find me at

Latest posts by Veselin Kenashkov (see all)

Share if you liked it

1 Reply to “Coroutines in Swoole”

Leave a Reply

Your email address will not be published. Required fields are marked *