The power of coroutine in Open Swoole: Go, Chan and Defer

Published:

Coroutine features are mature in Open Swoole since version 4.x. Swoole provides the powerful CSP (Communicating sequential processes) programming model with three keywords: go, chan, defer.

CSP is an alternative concurrent programming model to the actor model in Erlang or Akka. It is well known because it is adopted in Golang. The key concepts of CSP are:

  • Sequential processes
  • Synchronous communication through channels
  • Multiplexing of channels with alternation

Version used in this article: Swoole 4.2.9 PHP 7.2.9

Keywords

  • go, Create a new coroutine
  • chan, Create a new channel for message-passing
  • defer, Delay the task until the exit of the coroutine, FIFO

There is no I/O waste within these three keywords. It is as fast as use Array in PHP. Compare with socket or file operations which are blocking the I/O, these three keywords are very fast and cheap.

Coroutine concurrency in Open Swoole

The keyword go creates a new coroutine for a function, then the function is running concurrently. If you like to concurrently run a piece of code, just have to put the function into a go wrapper.

Sequential processing
<?php
function test1()
{
    sleep(1);
    echo "b";
}

function test2()
{
    sleep(2);
    echo "c";
}

test1();
test2();

Result

time php b1.php
bc
real    0m3.080s
user    0m0.016s
sys     0m0.063s

The function test2 is running after test1. So the whole script needs three seconds to be finished.

Concurrent processing

Now let's see the magic of go, we can put the above test1 and test2 function into go().

<?php
Swoole\Runtime::enableCoroutine();

go(function ()
{
    sleep(1);
    echo "b";
});

go(function ()
{
    sleep(2);
    echo "c";
});

Swoole\Runtime::enableCoroutine() switch the PHP build-in stream, sleep, pdo, mysqli, redis from blocking model to be async model with Swoole Coroutine.

Result

time php co.php
bc
real    0m2.076s
user    0m0.000s
sys     0m0.078s

We can see the whole script only takes 2 seconds which is the time cost of function test2.

Time cost for sequential processing vs concurrent processing

  • Sequential processing: t1+t2+t2+....
  • Concurrent processing: max(t1, t2, t3, ...)

Coroutine communication

It is easy to write concurrent programs with go feature in Open Swoole. The problem we are facing now is how this coroutine collaborates with each other.

Channel can be created in Open Swoole for exchanging data between different coroutines.

A channel is created by keyword chan, you are able to choose the size of the channel:

<?php
$chan = new chan(2);

There are two methods can be used to operate the chan:

<?php
$chan->pop(); // Read data from the channel, it will block and wait if the channel is empty
$chan->push(); // Write data into the channel, it will block and wait if the channel is full

Let's see a real-world use case: concurrently fetch two web pages https://www.google.com and https://www.bing.com. Keep it simple we only fetch the status code of HTTP responses:

<?php
// Create a channel with size 2 and try to read the final result
$chan = new chan(2);

go(function () use ($chan) {
    $result = [];
    for ($i = 0; $i < 2; $i++)
    {
        $result += $chan->pop();
    }
    var_dump($result);
});

// Start fetching the first webpage without blocking the script
go(function () use ($chan) {
   $cli = new Swoole\Coroutine\Http\Client('www.google.com', 80);
       $cli->set(['timeout' => 10]);
       $cli->setHeaders([
       'Host' => "www.google.com",
       "User-Agent" => 'Chrome/49.0.2587.3',
       'Accept' => 'text/html,application/xhtml+xml,application/xml',
       'Accept-Encoding' => 'gzip',
   ]);
   $ret = $cli->get('/');
   $chan->push(['www.google.com' => $cli->statusCode]);
});

// Start fetching the second webpage without blocking the script
go(function () use ($chan) {
   $cli = new Swoole\Coroutine\Http\Client('www.bing.com', 80);
   $cli->set(['timeout' => 10]);
   $cli->setHeaders([
       'Host' => "www.bing.com",
       "User-Agent" => 'Chrome/49.0.2587.3',
       'Accept' => 'text/html,application/xhtml+xml,application/xml',
       'Accept-Encoding' => 'gzip',
   ]);
   $ret = $cli->get('/');
   $chan->push(['www.bing.com' => $cli->statusCode]);
});

Result

time php co2.php
array(2) {
  ["www.google.com"]=>
  int(200)
  ["www.bing.com"]=>
  int(200)
}

real    0m0.268s
user    0m0.016s
sys     0m0.109s

We have created three coroutine with the keyword go. The first one is trying to get the final result from the other two task coroutines. We used chan for the communication between these three coroutines:

  • Coroutine 1: trying to read two results, it is waiting because the channel is empty.
  • Coroutine 2 and 3: fetching the webpage and push the result into channel then exit.

Once Coroutine 1 gets the result from the other two, the process will go to the next step and exit the script.

Delayed jobs by defer

defer can be used to do some final tasks when coroutines finished the task. Similar to register_shutdown_function in PHP.

<?php
Swoole\Runtime::enableCoroutine();

go(function () {
    echo "a";
    defer(function () {
        echo "~a";
    });
    echo "b";
    defer(function () {
        echo "~b";
    });
    sleep(1);
    echo "c";
});

Result

time php defer.php
abc~b~a
real    0m1.068s
user    0m0.016s
sys     0m0.047s

We can see the sequence of the results: a,b,c then the last deferred task ~b, finally the first deferred task ~a. More about defer, please check Coroutine in Open Swoole 4.x vs Coroutine in Golang

Conclusion

Go, Chan and Defer are provided since Open Swoole 4.x, it provides a brand new programming model in PHP: CSP. It can be used for writing TCP Server, UDP Server, HTTP Server with PHP language or PHP CLI scripts and speed up the tasking related to I/O.