生成器委托
简单地翻译官方文档的描述:
PHP7中,通过生成器委托(yield from),可以将其他生成器、可迭代的对象、数组委托给外层生成器。外层的生成器会先顺序 yield 委托出来的值,然后继续 yield 本身中定义的值。
利用 yield from 可以方便我们编写比较清晰生成器嵌套,而代码嵌套调用是编写复杂系统所必需的。
上例子:
1 <?php 2 function echoTimes($msg, $max) { 3 for ($i = 1; $i <= $max; ++$i) { 4 echo "$msg iteration $i "; 5 yield; 6 } 7 } 8 9 function task() { 10 yield from echoTimes('foo', 10); // print foo ten times 11 echo "--- "; 12 yield from echoTimes('bar', 5); // print bar five times 13 } 14 15 foreach (task() as $item) { 16 ; 17 }
以上将输出:
1 foo iteration 1 2 foo iteration 2 3 foo iteration 3 4 foo iteration 4 5 foo iteration 5 6 foo iteration 6 7 foo iteration 7 8 foo iteration 8 9 foo iteration 9 10 foo iteration 10 11 --- 12 bar iteration 1 13 bar iteration 2 14 bar iteration 3 15 bar iteration 4 16 bar iteration 5
自然,内部生成器也可以接受它的父生成器发送的信息或者异常,因为 yield from 为父子生成器建立一个双向的通道。不多说,上例子:
1 <?php 2 function echoMsg($msg) { 3 while (true) { 4 $i = yield; 5 if($i === null){ 6 break; 7 } 8 if(!is_numeric($i)){ 9 throw new Exception("Hoo! must give me a number"); 10 } 11 echo "$msg iteration $i "; 12 } 13 } 14 function task2() { 15 yield from echoMsg('foo'); 16 echo "--- "; 17 yield from echoMsg('bar'); 18 } 19 $gen = task2(); 20 foreach (range(1,10) as $num) { 21 $gen->send($num); 22 } 23 $gen->send(null); 24 foreach (range(1,5) as $num) { 25 $gen->send($num); 26 } 27 //$gen->send("hello world"); //try it ,gay
输出和上个例子是一样的。
生成器返回值
如果生成器被迭代完成,或者运行到 return 关键字,是会给这个生成器返回值的。
可以有两种方法获取这个返回值:
- 使用 $ret = Generator::getReturn() 方法。
- 使用 $ret = yield from Generator() 表达式。
上例子:
1 <?php 2 function echoTimes($msg, $max) { 3 for ($i = 1; $i <= $max; ++$i) { 4 echo "$msg iteration $i "; 5 yield; 6 } 7 return "$msg the end value : $i "; 8 } 9 10 function task() { 11 $end = yield from echoTimes('foo', 10); 12 echo $end; 13 $gen = echoTimes('bar', 5); 14 yield from $gen; 15 echo $gen->getReturn(); 16 } 17 18 foreach (task() as $item) { 19 ; 20 }
输出结果就不贴了,想必大家都猜到。
可以看到 yield from 和 return 结合使得 yield 的写法更像平时我们写的同步模式的代码了,毕竟,这就是 PHP 出生成器特性的原因之一呀。
一个非阻塞的web服务器
时间回到2015年,鸟哥博客上转载的一篇《 在PHP中使用协程实现多任务调度》。文章介绍了PHP5 的迭代生成器,协程,并实现了一个简单的非阻塞 web 服务器。(链接见文末引用)
现在我们利用 PHP7 中的这两个新特性重写这个 web 服务器,只需要 100 多行代码。
代码如下:
1 <?php 2 3 class CoSocket 4 { 5 protected $masterCoSocket = null; 6 public $socket; 7 protected $handleCallback; 8 public $streamPoolRead = []; 9 public $streamPoolWrite = []; 10 11 public function __construct($socket, CoSocket $master = null) 12 { 13 $this->socket = $socket; 14 $this->masterCoSocket = $master ?? $this; 15 } 16 17 public function accept() 18 { 19 $isSelect = yield from $this->onRead(); 20 $acceptS = null; 21 if ($isSelect && $as = stream_socket_accept($this->socket, 0)) { 22 $acceptS = new CoSocket($as, $this); 23 } 24 return $acceptS; 25 } 26 27 public function read($size) 28 { 29 yield from $this->onRead(); 30 yield ($data = fread($this->socket, $size)); 31 return $data; 32 } 33 34 public function write($string) 35 { 36 yield from $this->onWriter(); 37 yield fwrite($this->socket, $string); 38 } 39 40 public function close() 41 { 42 unset($this->masterCoSocket->streamPoolRead[(int)$this->socket]); 43 unset($this->masterCoSocket->streamPoolWrite[(int)$this->socket]); 44 yield ($success = @fclose($this->socket)); 45 return $success; 46 } 47 48 public function onRead($timeout = null) 49 { 50 $this->masterCoSocket->streamPoolRead[(int)$this->socket] = $this->socket; 51 $pool = $this->masterCoSocket->streamPoolRead; 52 $rSocks = []; 53 $wSocks = $eSocks = null; 54 foreach ($pool as $item) { 55 $rSocks[] = $item; 56 } 57 yield ($num = stream_select($rSocks, $wSocks, $eSocks, $timeout)); 58 return $num; 59 } 60 61 public function onWriter($timeout = null) 62 { 63 $this->masterCoSocket->streamPoolWrite[(int)$this->socket] = $this->socket; 64 $pool = $this->masterCoSocket->streamPoolRead; 65 $wSocks = []; 66 $rSocks = $eSocks = null; 67 foreach ($pool as $item) { 68 $wSocks[] = $item; 69 } 70 yield ($num = stream_select($rSocks, $wSocks, $eSocks, $timeout)); 71 return $num; 72 } 73 74 public function onRequest() 75 { 76 /** @var self $socket */ 77 $socket = yield from $this->accept(); 78 if (empty($socket)) { 79 return false; 80 } 81 $data = yield from $socket->read(8192); 82 $response = call_user_func($this->handleCallback, $data); 83 yield from $socket->write($response); 84 return yield from $socket->close(); 85 } 86 87 public static function start($port, callable $callback) 88 { 89 echo "Starting server at port $port... "; 90 $socket = @stream_socket_server("tcp://0.0.0.0:$port", $errNo, $errStr); 91 if (!$socket) throw new Exception($errStr, $errNo); 92 stream_set_blocking($socket, 0); 93 $coSocket = new self($socket); 94 $coSocket->handleCallback = $callback; 95 function gen($coSocket) 96 { 97 /** @var self $coSocket */ 98 while (true) yield from $coSocket->onRequest(); 99 } 100 foreach (gen($coSocket) as $item){}; 101 } 102 } 103 104 CoSocket::start(8000, function ($data) { 105 $response = <<<RES 106 HTTP/1.1 200 OK 107 Content-Type: text/plain 108 Content-Length: 12 109 Connection: close 110 111 hello world! 112 RES; 113 return $response; 114 });