Initial PHP experience and initial php experience
PHP coroutine initial experience
By warezhou 2014.11.24
Since the last attempt to add coroutine to PHP through the C extension failed, Zend is almost zero in the short term and can only be used as a native language. After Google found that PHP5.5 introduced the new features of Generator and Coroutine, so this article was born.
Background
When C/C ++ background development encounters Coroutine
Http://blog.csdn.net/cszhouwei/article/details/14230529
A failed PHP extension development tour
Http://blog.csdn.net/cszhouwei/article/details/41290673
Prerequisites Generator
function my_range($start, $end, $step = 1) { for ($i = $start; $i <= $end; $i += $step) { yield $i; }}foreach (my_range(1, 1000) as $num) { echo $num, "\n";}/* * 1 * 2 * ... * 1000 */
Figure 1 implement range () based on generator
$range = my_range(1, 1000);var_dump($range);/* * object(Generator)#1 (0) { * } */var_dump($range instanceof Iterator);/* * bool(true) */
Figure 2 speculation about the implementation of my_range ()
Since I have been familiar with PHP for a long time and have not gone deep into the language implementation details, I can only guess based on the phenomenon. Here are some of my personal understandings:
- A function that contains the yield keyword is special. The returned value is a Generator object. At this time, the statements in the function are not actually executed.
- The Generator object is an Iterator interface instance, which can be manipulated through rewind (), current (), next (), and valid () interfaces.
- Generator can be considered as an "interruptible" function, and yield forms a series of "Medium breakpoints"
- Generator is similar to the assembly line produced in the workshop. It gets one from it every time you use the product. Then the assembly line stops there waiting for the next operation.
Coroutine
Careful readers may have discovered that as of now, Generator has implemented the key features of Coroutine: interrupted execution and resumed execution. According to the idea of "when C/C ++ backend development encounters Coroutine", it is sufficient to use the "global variables" language facility for information transmission to Implement Asynchronous Server.
In fact, compared with the swapcontext family functions, Generator has made a huge step forward, and has the ability to "return data". If both have the ability to "send data, you don't have to make a detour through the lame techniques any more. In PHP, you can use the send () interface of Generator (Note: it is no longer the next () Interface) to complete the "send data" task, thus achieving real "two-way communication ".
function gen() { $ret = (yield 'yield1'); echo "[gen]", $ret, "\n"; $ret = (yield 'yield2'); echo "[gen]", $ret, "\n";}$gen = gen();$ret = $gen->current();echo "[main]", $ret, "\n";$ret = $gen->send("send1");echo "[main]", $ret, "\n";$ret = $gen->send("send2");echo "[main]", $ret, "\n";/* * [main]yield1 * [gen]send1 * [main]yield2 * [gen]send2 * [main] */
Figure 3 Coroutine bidirectional communication example
As a C/C ++ code farmer, after discovering "re-entry" and "two-way communication" capabilities, it seems that there is no more luxury, but PHP is generous, the Exception mechanism is added, and the "error handling" mechanism is further improved.
function gen() { $ret = (yield 'yield1'); echo "[gen]", $ret, "\n"; try { $ret = (yield 'yield2'); echo "[gen]", $ret, "\n"; } catch (Exception $ex) { echo "[gen][Exception]", $ex->getMessage(), "\n"; } echo "[gen]finish\n";}$gen = gen();$ret = $gen->current();echo "[main]", $ret, "\n";$ret = $gen->send("send1");echo "[main]", $ret, "\n";$ret = $gen->throw(new Exception("Test"));echo "[main]", $ret, "\n";/* * [main]yield1 * [gen]send1 * [main]yield2 * [gen][Exception]Test * [gen]finish * [main] */
Figure 4 Coroutine error handling example
Practical drills
The previous section briefly introduced the relevant language facilities. How should we use them in actual projects? Let's continue with the scenario described in "A failed PHP extension development journey" and use the above features to achieve that beautiful wish: Write asynchronous code in synchronous mode!
First Draft
<?phpclass AsyncServer { protected $handler; protected $socket; protected $tasks = []; public function __construct($handler) { $this->handler = $handler; $this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); if(!$this->socket) { die(socket_strerror(socket_last_error())."\n"); } if (!socket_set_nonblock($this->socket)) { die(socket_strerror(socket_last_error())."\n"); } if(!socket_bind($this->socket, "0.0.0.0", 1234)) { die(socket_strerror(socket_last_error())."\n"); } } public function Run() { while (true) { $reads = array($this->socket); foreach ($this->tasks as list($socket)) { $reads[] = $socket; } $writes = NULL; $excepts= NULL; if (!socket_select($reads, $writes, $excepts, 0, 1000)) { continue; } foreach ($reads as $one) { $len = socket_recvfrom($one, $data, 65535, 0, $ip, $port); if (!$len) { //echo "socket_recvfrom fail.\n"; continue; } if ($one == $this->socket) { //echo "[Run]request recvfrom succ. data=$data ip=$ip port=$port\n"; $handler = $this->handler; $coroutine = $handler($one, $data, $len, $ip, $port); $task = $coroutine->current(); //echo "[Run]AsyncTask recv. data=$task->data ip=$task->ip port=$task->port timeout=$task->timeout\n"; $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); if(!$socket) { //echo socket_strerror(socket_last_error())."\n"; $coroutine->throw(new Exception(socket_strerror(socket_last_error()), socket_last_error())); continue; } if (!socket_set_nonblock($socket)) { //echo socket_strerror(socket_last_error())."\n"; $coroutine->throw(new Exception(socket_strerror(socket_last_error()), socket_last_error())); continue; } socket_sendto($socket, $task->data, $task->len, 0, $task->ip, $task->port); $this->tasks[$socket] = [$socket, $coroutine]; } else { //echo "[Run]response recvfrom succ. data=$data ip=$ip port=$port\n"; if (!isset($this->tasks[$one])) { //echo "no async_task found.\n"; } else { list($socket, $coroutine) = $this->tasks[$one]; unset($this->tasks[$one]); socket_close($socket); $coroutine->send(array($data, $len)); } } } } }}class AsyncTask { public $data; public $len; public $ip; public $port; public $timeout; public function __construct($data, $len, $ip, $port, $timeout) { $this->data = $data; $this->len = $len; $this->ip = $ip; $this->port = $port; $this->timeout = $timeout; }}function RequestHandler($socket, $req_buf, $req_len, $ip, $port) { //echo "[RequestHandler] before yield AsyncTask. REQ=$req_buf\n"; list($rsp_buf, $rsp_len) = (yield new AsyncTask($req_buf, $req_len, "127.0.0.1", 2345, 1000)); //echo "[RequestHandler] after yield AsyncTask. RSP=$rsp_buf\n"; socket_sendto($socket, $rsp_buf, $rsp_len, 0, $ip, $port);}$server = new AsyncServer(RequestHandler);$server->Run();?>
Code explanation:
- For ease of Problem description, all the underlying communication here is based on UDP, and tedious details such as TCP connect are omitted.
- AsyncServer is the underlying framework class. It encapsulates network communication details and coroutine switching details and binds coroutine through socket.
- RequestHandler is a service processing function and implements Asynchronous Network interaction through yield new AsyncTask ().
Improved Version 2
Issues left behind in the first version:
- The timeout of Asynchronous Network interaction is not implemented. Only interface parameters are reserved.
- Yield new AsyncTask () calling method is not natural enough, slightly awkward
<?phpclass AsyncServer { protected $handler; protected $socket; protected $tasks = []; protected $timers = []; public function __construct(callable $handler) { $this->handler = $handler; $this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); if(!$this->socket) { die(socket_strerror(socket_last_error())."\n"); } if (!socket_set_nonblock($this->socket)) { die(socket_strerror(socket_last_error())."\n"); } if(!socket_bind($this->socket, "0.0.0.0", 1234)) { die(socket_strerror(socket_last_error())."\n"); } } public function Run() { while (true) { $now = microtime(true) * 1000; foreach ($this->timers as $time => $sockets) { if ($time > $now) break; foreach ($sockets as $one) { list($socket, $coroutine) = $this->tasks[$one]; unset($this->tasks[$one]); socket_close($socket); $coroutine->throw(new Exception("Timeout")); } unset($this->timers[$time]); } $reads = array($this->socket); foreach ($this->tasks as list($socket)) { $reads[] = $socket; } $writes = NULL; $excepts= NULL; if (!socket_select($reads, $writes, $excepts, 0, 1000)) { continue; } foreach ($reads as $one) { $len = socket_recvfrom($one, $data, 65535, 0, $ip, $port); if (!$len) { //echo "socket_recvfrom fail.\n"; continue; } if ($one == $this->socket) { //echo "[Run]request recvfrom succ. data=$data ip=$ip port=$port\n"; $handler = $this->handler; $coroutine = $handler($one, $data, $len, $ip, $port); if (!$coroutine) { //echo "[Run]everything is done.\n"; continue; } $task = $coroutine->current(); //echo "[Run]AsyncTask recv. data=$task->data ip=$task->ip port=$task->port timeout=$task->timeout\n"; $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); if(!$socket) { //echo socket_strerror(socket_last_error())."\n"; $coroutine->throw(new Exception(socket_strerror(socket_last_error()), socket_last_error())); continue; } if (!socket_set_nonblock($socket)) { //echo socket_strerror(socket_last_error())."\n"; $coroutine->throw(new Exception(socket_strerror(socket_last_error()), socket_last_error())); continue; } socket_sendto($socket, $task->data, $task->len, 0, $task->ip, $task->port); $deadline = $now + $task->timeout; $this->tasks[$socket] = [$socket, $coroutine, $deadline]; $this->timers[$deadline][$socket] = $socket; } else { //echo "[Run]response recvfrom succ. data=$data ip=$ip port=$port\n"; list($socket, $coroutine, $deadline) = $this->tasks[$one]; unset($this->tasks[$one]); unset($this->timers[$deadline][$one]); socket_close($socket); $coroutine->send(array($data, $len)); } } } }}class AsyncTask { public $data; public $len; public $ip; public $port; public $timeout; public function __construct($data, $len, $ip, $port, $timeout) { $this->data = $data; $this->len = $len; $this->ip = $ip; $this->port = $port; $this->timeout = $timeout; }}function AsyncSendRecv($req_buf, $req_len, $ip, $port, $timeout) { return new AsyncTask($req_buf, $req_len, $ip, $port, $timeout);}function RequestHandler($socket, $req_buf, $req_len, $ip, $port) { //echo "[RequestHandler] before yield AsyncTask. REQ=$req_buf\n"; try { list($rsp_buf, $rsp_len) = (yield AsyncSendRecv($req_buf, $req_len, "127.0.0.1", 2345, 3000)); } catch (Exception $ex) { $rsp_buf = $ex->getMessage(); $rsp_len = strlen($rsp_buf); //echo "[Exception]$rsp_buf\n"; } //echo "[RequestHandler] after yield AsyncTask. RSP=$rsp_buf\n"; socket_sendto($socket, $rsp_buf, $rsp_len, 0, $ip, $port);}$server = new AsyncServer(RequestHandler);$server->Run();?>
Code explanation:
- With the built-in array capability of PHP, simple "time-out management" is implemented, with millisecond as the precision of time sharding
- Encapsulate the AsyncSendRecv interface, and the call is more natural, such as yield AsyncSendRecv ().
- Add Exception as the error handling mechanism and add ret_code for demonstration only
Performance testing environment
Test Data
|
100 Byte/REQ |
1000 Byte/REQ |
Async_svr_v1.php |
16000/s |
15000/s |
Async_svr_v2.php |
11000/s |
10000/s |
Future Prospects
- Interested PHPer can encapsulate the underlying framework based on this idea and encapsulate common blocking operations, such as connect, send, recv, and sleep...
- I have been familiar with PHP for a long time, and many of them are not optimal in usage. Experts can optimize them accordingly and their performance should be improved.
- Currently, coroutine binding is based on socket. If the connection pool is too overhead for each connect/close operation based on TCP communication, you need to consider implementing the connection pool.
- Similar language facilities are also available for python and other languages. Interested readers can study them on their own.