如果是做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
进程已结束,退出代码为 01:可能你在这发现了几个东西,和之前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