如果是做Python或者其他语言的小伙伴,对于生成器应该不陌生。但很多PHP开发者或许都不知道生成器这个功能,可能是因为生成器是PHP 5.5.0才引入的功能,也可以是生成器作用不是很明显。但是,生成器功能的确非常有用。
iterator迭代器
generator 生成器
直接讲概念估计你听完还是一头雾水,所以我们先来说说优点,也许能勾起你的兴趣。那么生成器有哪些优点,如下:
生成器会对PHP应用的性能有非常大的影响
PHP代码运行时节省大量的内存
比较适合计算大量的数据
那么,这些神奇的功能究竟是如何做到的?我们先来举个例子。
首先,放下生成器概念的包袱,来看一个简单的PHP函数:
function createRange($number){ $data = []; for($i=0;$i<$number;$i++){ $data[] = time(); } return $data; }
这是一个非常常见的PHP函数,我们在处理一些数组的时候经常会使用。这里的代码也非常简单:
我们创建一个函数。
函数内包含一个for
循环,我们循环的把当前时间放到$data
里面
for
循环执行完毕,把$data
返回出去。
下面没完,我们继续。我们再写一个函数,把这个函数的返回值循环打印出来:
$result = createRange(10); // 这里调用上面我们创建的函数 foreach($result as $value){ sleep(1);//这里停顿1秒,我们后续有用 echo $value.'<br />'; }
我们在浏览器里面看一下运行结果:
这里非常完美,没有任何问题。(当然sleep(1)
效果你们看不出来)
我们注意到,在调用函数createRange
的时候给$number
的传值是10,一个很小的数字。假设,现在传递一个值10000000
(1000万)。
那么,在函数createRange
里面,for
循环就需要执行1000
万次。且有1000
万个值被放到$data
里面,而$data
数组在是被放在内存内。所以,在调用函数时候会占用大量内存。
这里,生成器就可以大显身手了。
我们直接修改代码,你们注意观察:
function createRange($number){ for($i=0;$i<$number;$i++){ yield time(); } }
看下这段和刚刚很像的代码,我们删除了数组$data
,而且也没有返回任何内容,而是在time()
之前使用了一个关键字yield
我们再运行一下第二段代码:
$result = createRange(10); // 这里调用上面我们创建的函数 foreach($result as $value){ sleep(1); echo $value.'<br />'; }
我们奇迹般的发现了,输出的值和第一次没有使用生成器的不一样。这里的值(时间戳)中间间隔了1秒。
这里的间隔一秒其实就是sleep(1)
造成的后果。但是为什么第一次没有间隔?那是因为:
未使用生成器时:createRange
函数内的for
循环结果被很快放到$data
中,并且立即返回。所以,foreach
循环的是一个固定的数组。
使用生成器时:createRange
的值不是一次性快速生成,而是依赖于foreach
循环。foreach
循环一次,for
执行一次。
到这里,你应该对生成器有点儿头绪。
到这里,你应该已经大概理解什么是生成器了。下面我们来说下生成器原理。
首先明确一个概念:生成器yield关键字不是返回值,他的专业术语叫产出值,只是生成一个值
那么代码中foreach
循环的是什么?其实是PHP在使用生成器的时候,会返回一个Generator
类的对象。foreach
可以对该对象进行迭代,每一次迭代,PHP会通过Generator
实例计算出下一次需要迭代的值。这样foreach
就知道下一次需要迭代的值了。
而且,在运行中for
循环执行后,会立即停止。等待foreach
下次循环时候再次和for
索要下次的值的时候,for
循环才会再执行一次,然后立即再次停止。直到不满足条件不执行结束。
很多PHP开发者不了解生成器,其实主要是不了解应用领域。那么,生成器在实际开发中有哪些应用?
PHP开发很多时候都要读取大文件,比如csv文件、text文件,或者一些日志文件。这些文件如果很大,比如5个G。这时,直接一次性把所有的内容读取到内存中计算不太现实。
这里生成器就可以派上用场啦。简单看个例子:读取text文件
我们创建一个text文本文档,并在其中输入几行文字,示范读取。
<?php header("content-type:text/html;charset=utf-8"); function readTxt() { # code... $handle = fopen("./test.txt", 'rb'); while (feof($handle)===false) { # code... yield fgets($handle); } fclose($handle); } foreach (readTxt() as $key => $value) { # code... echo $value.'<br />'; }
通过上图的输出结果我们可以看出代码完全正常。
但是,背后的代码执行规则却一点儿也不一样。使用生成器读取文件,第一次读取了第一行,第二次读取了第二行,以此类推,每次被加载到内存中的文字只有一行,大大的减小了内存的使用。
这样,即使读取上G的文本也不用担心,完全可以像读取很小文件一样编写代码。
在php中,除了数组,对象可以被foreach遍历之外,还有另外一种特殊对象,也就是继承了iterator接口的对象,也可以被对象遍历,但和普通对象的遍历又有所不同.
class Interatorobj implements Iterator { protected $data; public function __construct($array) { $this->data = $array; } public function rewind() { // TODO: Implement rewind() method. echo "指针重置\n"; reset($this->data); } public function current() { // TODO: Implement current() method. echo "当前指针数据\n"; return current($this->data). "自定义遍历值"; } public function next() { // TODO: Implement next() method. echo "指针下移\n"; next($this->data); } public function key() { // TODO: Implement key() method. echo "返回当前数组键值\n"; return key($this->data); } public function valid() { // TODO: Implement valid() method. echo "检查迭代器指针是否正常\n"; return current($this->data); } } foreach (new Interatorobj( array( 'a'=>1, 'b'=>2, 'c'=>3, )) as $key=>$value) { echo "迭代器自定义遍历\n"; var_dump($key,$value); }
可以看出,迭代器的遍历,会依次调用重置,检查当前数据,返回当前指针数据,指针下移方法,结束遍历的条件在于检查数据返回true或者false
生成器和迭代器类似,但也完全不同
生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值。
生成器使用yield关键字进行生成迭代的值
function xrange($start, $limit ,$step) { if($start < $limit) { if($step <= 0) { throw new LogicException("step 必须大于0"); } else { for ($i=$start; $i<=$limit; $i+=$step) { yield $i; //break; //只遍历一次对象 } } } else { if($step >= 0) { throw new LogicException("step 必须小于0"); } else { for ($i=$start; $i>=$limit; $i+=$step) { yield $i; } } } } var_dump(xrange(1,9,2)); foreach (xrange(1,9,2) as $key=>$value) { var_dump($value); }
输出结果:
object(Generator)#1 (0) { } int(1) int(3) int(5) int(7) int(9) 进程已结束,退出代码为 0
生成器它的内部实现了以下方法:
Generator implements Iterator { //返回当前产生的值 public mixed current ( void ) //返回当前产生的键 public mixed key ( void ) //生成器继续执行 public void next ( void ) //重置迭代器,如果迭代已经开始了,这里会抛出一个异常。 public void rewind ( void ) //向生成器中传入一个值,当前yield接收值,然后继续执行下一个yield public mixed send ( mixed $value ) //向生成器中抛入一个异常 public void throw ( Exception $exception ) //检查迭代器是否被关闭,已被关闭返回 FALSE,否则返回 TRUE public bool valid ( void ) //序列化回调 public void __wakeup ( void ) //返回generator函数的返回值,PHP version 7+ public mixed getReturn ( void ) }
生成器的语法有很多种用法,需要一一说明,首先,yield必须有函数包裹,包裹yield的函数称为"生成器函数",该函数将返回一个可遍历的对象
1:颠覆常识的yield
function Generator() { for ($i=0; $i<3; $i++) { echo "输出存在感1\n"; yield $i; echo "输出存在感2\n"; } } echo "#######返回对象#########\n"; var_dump(Generator());//返回对象 echo "\n#######返回对象#########\n"; echo "#######遍历一次情况#########\n"; foreach (Generator() as $key=>$value) { var_dump($value); break; } echo "\n#######返回对象#########\n"; echo "#######一直遍历情况#########\n"; foreach (Generator() as $key=>$value) { var_dump($value); } 输出结果: #######返回对象######### object(Generator)#1 (0) { } #######返回对象######### #######遍历一次情况######### 输出存在感1 int(0) #######返回对象######### #######一直遍历情况######### 输出存在感1 int(0) 输出存在感2 输出存在感1 int(1) 输出存在感2 输出存在感1 int(2) 输出存在感2 进程已结束,退出代码为 0
1:可能你在这发现了几个东西,和之前php完全不同的认知;
2:在遍历一次的时候,可以发现调用函数,却没有正常的for循环3次,只循环了一次
3:在遍历一次的情况时,"存在感2"竟然没有调用,在一直遍历的情况下才调用
再看看另一个例子:
1:while(true)没有阻塞调用函数下面的代码执行,却导致了下面的echo "额额额"和return 无法执行
2:return 返回值竟然是没有作用的
3:send(1)时,没有echo "哈哈",send(2)时,才开始出现"哈哈",
2:yield的其他语法
yield表达式中,也可以赋值,但赋值需要使用括号包裹:
只需要在表达式后面加上$key=>$value,即可生成键值的数据:
在函数前增加引用定义,就可以像returning references from functions(从函数返回一个引用)一样 引用生成值
三:特性总结
1:yield是生成器所需要的关键字,必须在函数内部,有yield的函数叫做"生成器函数"
2:调用生成器函数时,函数将返回一个继承了Iterator的生成器
3:yield作为表达式使用时,可将一个值加入到生成器中进行遍历,遍历完会中断下面的语句运行,并且保存状态,当下次遍历时会继续执行(这就是while(true)没有造成阻塞的原因)
4:当send传入参数时,yield可作为一个变量使用,这个变量等于传入的参数
协程
一:实现个简单的协程
协程,是一种编程逻辑的转变,使多个任务能交替运行,而不是之前的一直根据流程往下走,举个例子
当有一个逻辑,每次调用这个文件时,该文件要做3件事:
1:写入300个文件
2:发送邮件给500个会员
3:插入100条数据
/** * 任务对象 * Class Task */ class Task { protected $taskId;//任务id protected $coroutine;//生成器 protected $sendValue = null;//生成器send值 protected $beforeFirstYield = true;//迭代指针是否是第一个 public function __construct($taskId, Generator $coroutine) { $this->taskId = $taskId; $this->coroutine = $coroutine; } public function getTaskId() { return $this->taskId; } /** * 设置插入数据 * @param $sendValue */ public function setSendValue($sendValue) { $this->sendValue = $sendValue; } /** * send数据进行迭代 * @return mixed */ public function run() { //如果是 if ($this->beforeFirstYield) { $this->beforeFirstYield = false; var_dump($this->coroutine->current()); return $this->coroutine->current(); } else { $retval = $this->coroutine->send($this->sendValue); $this->sendValue = null; return $retval; } } /** * 是否完成 * @return bool */ public function isFinished() { return !$this->coroutine->valid(); } }
这个封装类,可以更好的去调用运行生成器函数,但只有这个也是不够的,我们还需要一个调度任务类,来代替前面的while:
/** * 任务调度 * Class Scheduler */ class Scheduler { protected $maxTaskId = 0;//任务id protected $taskMap = []; // taskId => task protected $taskQueue;//任务队列 public function __construct() { $this->taskQueue = new SplQueue(); } public function newTask(Generator $coroutine) { $tid = ++$this->maxTaskId; //新增任务 $task = new Task($tid, $coroutine); $this->taskMap[$tid] = $task; $this->schedule($task); return $tid; } /** * 任务入列 * @param Task $task */ public function schedule(Task $task) { $this->taskQueue->enqueue($task); } public function run() { while (!$this->taskQueue->isEmpty()) { //任务出列进行遍历生成器数据 $task = $this->taskQueue->dequeue(); $task->run(); if ($task->isFinished()) { //完成则删除该任务 unset($this->taskMap[$task->getTaskId()]); } else { //继续入列 $this->schedule($task); } } } }
很好,我们已经有了一个调度类,还有了一个任务类,可以继续实现上面的功能了:
function task1() { for ($i = 0; $i <= 300; $i++) { //写入文件,大概要3000微秒 usleep(3000); echo "写入文件{$i}\n"; yield $i; } } function task2() { for ($i = 0; $i <= 500; $i++) { //发送邮件给500名会员,大概3000微秒 usleep(3000); echo "发送邮件{$i}\n"; yield $i; } } function task3() { for ($i = 0; $i <= 100; $i++) { //模拟插入100条数据,大概3000微秒 usleep(3000); echo "插入数据{$i}\n"; yield $i; } } $scheduler = new Scheduler; $scheduler->newTask(task1()); $scheduler->newTask(task2()); $scheduler->newTask(task3()); $scheduler->run();
很好,我们已经实现了可以调度任务,进行任务交叉运行的功能了,
这就是"协程"协程可以将多个不同的任务交叉运行
二:协程与调度器的通信
我们在上面已经实现了一个协程封装了,但是任务和调度器缺少了通信,我们可以重新封装下,使协程当中能够获取当前的任务id,新增任务,以及杀死任务
先封装一下调用的封装:
class YieldCall{ protected $callback; public function __construct(callable $callback) { $this->callback = $callback; } /** * 调用时将返回结果 * @param Task $task * @param Scheduler $scheduler * @return mixed */ public function __invoke(Task $task, Scheduler $scheduler) { $callback = $this->callback; return $callback($task, $scheduler); } }
同时我们需要小小的改动下调度器的run方法:
public function run() { while (!$this->taskQueue->isEmpty()) { $task = $this->taskQueue->dequeue(); $retval = $task->run(); //如果返回的是YieldCall实例,则先执行 if ($retval instanceof YieldCall) { $retval($task, $this); continue; } if ($task->isFinished()) { unset($this->taskMap[$task->getTaskId()]); } else { $this->schedule($task); } } }
新增 getTaskId函数去返回task_id:
function getTaskId(){ //返回一个YieldCall的实例 return new YieldCall( //该匿名函数会先获取任务id,然后send给生成器,并且由YieldCall将task_id返回给生成器函数 function (Task $task, Scheduler $scheduler) { $task->setSendValue($task->getTaskId()); $scheduler->schedule($task); } ); }
然后,我们再修改下task1,task2,task3函数:
function task1(){ $task_id = (yield getTaskId()); for ($i = 0; $i <= 300; $i++) { //写入文件,大概要3000微秒 usleep(3000); echo "任务{$task_id}写入文件{$i}\n"; yield $i; } } function task2(){ $task_id = (yield getTaskId()); for ($i = 0; $i <= 500; $i++) { //发送邮件给500名会员,大概3000微秒 usleep(3000); echo "任务{$task_id}发送邮件{$i}\n"; yield $i; } } function task3(){ $task_id = (yield getTaskId()); for ($i = 0; $i <= 100; $i++) { //模拟插入100条数据,大概3000微秒 usleep(3000); echo "任务{$task_id}插入数据{$i}\n"; yield $i; } } $scheduler = new Scheduler; $scheduler->newTask(task1()); $scheduler->newTask(task2()); $scheduler->newTask(task3()); $scheduler->run();
执行结果:
这样的话,当第一次执行的时候,会先调用getTaskId将task_id返回,然后将任务继续执行,这样,我们就获取到了调度器分配给任务的task_id,是不是很神奇?
三:生成新任务以及杀死任务
现在新增了一个需求:当发送邮件给会员时,需要新增一个发送短信的子任务,当会员id大于200时则停止。
同时,我们可以利用YieldCall,去新增任务和杀死任务:
/** * 传入一个生成器函数用于新增任务给调度器调用 * @param Generator $coroutine * @return YieldCall */function newTask(Generator $coroutine) { return new YieldCall( //该匿名函数,会在调度器中新增一个任务 function(Task $task, Scheduler $scheduler) use ($coroutine) { $task->setSendValue($scheduler->newTask($coroutine)); $scheduler->schedule($task); } ); }/** * 杀死一个任务 * @param $tid * @return YieldCall */function killTask($taskId) { return new YieldCall( //该匿名函数,传入一个任务id,然后让调度器去杀死该任务 function(Task $task, Scheduler $scheduler) use ($taskId) { $task->setSendValue($scheduler->killTask($taskId)); $scheduler->schedule($task); } ); }
同时,调度器也得有killTask的方法:
/** * 杀死一个任务 * @param $taskId * @return bool */public function killTask($taskId) { if (!isset($this->taskMap[$taskId])) { return false; } unset($this->taskMap[$taskId]); /** * 遍历队列,找出id相同的则删除 */ foreach ($this->taskQueue as $i => $task) { if ($task->getTaskId() === $taskId) { unset($this->taskQueue[$i]); break; } } return true; }
有了新增和删除,我们就可以重新写一下task2以及新增task4:
function task4(){ $task_id = (yield getTaskId()); while (true) { echo "任务{$task_id}发送短信\n"; yield; } }function task2(){ $task_id = (yield getTaskId()); $child_task_id = (yield newTask(task4())); for ($i = 0; $i <= 500; $i++) { //发送邮件给500名会员,大概3000微秒 usleep(3000); echo "任务{$task_id}发送邮件{$i}\n"; yield $i; if($i==200){ yield killTask($child_task_id); } } }
运行结果:
这样我们就完美的实现了新增任务,以及杀死任务了
总结
前面所说的,协程只是一种编程逻辑,一种写代码的技巧,协程能够帮助我们更好的切换代码中任务
从上面的例子不难发现,其实协程实现封装较为麻烦,并且不用协程也能实现这些功能,那为什么要用协程呢?
因为协程可以让代码更加的简洁,任务相互之间独立区分开,可以使代码更加的清爽
协程让我们可以更好的控制切换任务流
前面介绍了那么多,或许有很多人感觉不对,会说"协程不能提升效率吗?","协程到底用来干什么的?"
或许由上面的例子很难看出协程的用处,那我们继续举例子吧:
js ajax是phper都了解的一个技术,
当点击一个按钮时,先将点击事件ajax传输给后端进行增加一条点击数据,然后出现一个动画,这是一个很正常的事,那么请问,如果ajax是同步,并且在网络不好的情况,会发生什么呢?
没错,点击之后,页面将会卡几秒(网络不好),请求完毕之后,才会出现一个动画.
协程的用处就在这了,我们可以利用协程,把一些同步io等待的代码逻辑,改为异步,在等待的时间内,可以让cpu去处理其他任务,
就如同小学时候做的一道题:
小明烧开水需要10分钟,刷牙需要3分钟,吃早餐需要5分钟,请问做完这些事情总共需要多少分钟?
答案是10分钟,因为在烧开水这个步骤时,不需要坐在那里看水壶烧(异步,io耗时)可以先去刷牙,然后去吃早餐
以上就是php yield关于协程的全部内容了
swoole
由总结可以看出,协程用在最多的应用场景,在于需要io耗时,cpu可以节省出来的场景,并且必须要是异步操作
这里推荐swoole扩展https://www.swoole.com/ ,
easyswoole
EasySwoole 是一款基于Swoole Server 开发的常驻内存型的分布式PHP框架,专为API而生,摆脱传统PHP运行模式在进程唤起和文件加载上带来的性能损失。EasySwoole 高度封装了 Swoole Server 而依旧维持 Swoole Server 原有特性,支持同时混合监听HTTP、自定义TCP、UDP协议,让开发者以最低的学习成本和精力编写出多进程,可异步,高可用的应用服务。
协程有以下特点:
(1)协程的调度由应用程序调度器控制,调度器由开发应用程序者编写。协程在应用层面,进程和线程在操作系统层面。
(2)协作式的调度方式。由自己交出cpu执行权。
(3)减轻了OS处理零散任务和轻量级任务的负担。
(4)消耗更少的资源
你可能已经注意到调用current()之前没有调用rewind().这是因为生成迭代对象的时候已经隐含地执行了rewind操作.
function gen() { yield 'foo'; yield 'bar'; } $gen = gen(); var_dump($gen); var_dump($gen->send('something')); // 如之前提到的在send之前, 当$gen迭代器被创建的时候一个renwind()方法已经被隐式调用 // 所以实际上发生的应该类似: //$gen->rewind(); //var_dump($gen->send('something')); //这样renwind的执行将会导致第一个yield被执行, 并且忽略了他的返回值. //真正当我们调用yield的时候, 我们得到的是第二个yield的值! 导致第一个yield的值被忽略. //string(3) "bar"
生成器:
1. 迭代器iterator 将数据集合用对象的方式存储起来,使用foreach遍历迭代器实现数据集合的遍历。 2. 生成器与迭代器 生成器generator实现了迭代器,含有迭代器的方法。generator 是 forward-only 的迭代,在迭代开始后不能 rewind。生成器不能被实例 化,也就是直接new。通过含有yield关键字函数返回。在函数里面的yield构成了中断点。 3. yield关键字 可以理解为返回生成器函数的中断点,可以返回数据和向其发送数据。是生成器的关键所在。 4. 生成器与协程 协程的支持是在迭代生成器的基础上, 增加了可以回送数据给生成器的功能(调用者发送数据给被调用的生成器函数). 这就把生成器到调用 者的单向通信转变为两者之间的双向通信. 返回生成器的函数中函数体可以理解为一个协程。生成器调用成员函数控制协程的上下文执行
PHP7中,通过生成器委托(yield from),可以将其他生成器、可迭代的对象、数组委托给外层生成器。外层的生成器会先顺序 yield 委托出来的值,然后继续 yield 本身中定义的值。
利用 yield from 可以方便我们编写比较清晰生成器嵌套,而代码嵌套调用是编写复杂系统所必需的。
生成器返回值
如果生成器被迭代完成,或者运行到 return 关键字,是会给这个生成器返回值的。可以有两种方法获取这个返回值:
使用 $ret = Generator::getReturn() 方法。 使用 $ret = yield from Generator() 表达式。
进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。
协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。
PHP中的协程实现基础 yield
yield的根本实现是生成器类,而迭代器类是迭代器接口的实现:
参考:
本文为崔凯原创文章,转载无需和我联系,但请注明来自冷暖自知一抹茶ckhttp://www.cksite.cn