php的yield是在php5.5版本就出来了,而在初级php界却很少有人提起,我就说说个人对php yield的理解
Iterator接口
在php中,除了数组,对象可以被foreach遍历之外,还有另外一种特殊对象,也就是继承了iterator接口的对象,也可以被对象遍历,但和普通对象的遍历又有所不同,下面是3种类型的遍历情况:
可以看出,迭代器的遍历,会依次调用重置,检查当前数据,返回当前指针数据,指针下移方法,结束遍历的条件在于检查数据返回true或者false
生成器
生成器和迭代器类似,但也完全不同
生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值。
生成器使用yield关键字进行生成迭代的值
例如:
一:生成器方法
生成器它的内部实现了以下方法:
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
可能你在这发现了几个东西,和之前php完全不同的认知,如果你没发现,额,那我提出来吧
1:在调用函数返回的时候,可以发现for里面的语句并没有执行
2:在遍历一次的时候,可以发现调用函数,却没有正常的for循环3次,只循环了一次
3:在遍历一次的情况时,"存在感2"竟然没有调用,在一直遍历的情况下才调用
再看看另一个例子:
什么????while(ture)竟然还能正常的执行下去???没错,生成器函数就是这样的,根据这个例子,我们发现了这些东西:
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条数据
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<?php
function
task1(){
for
(
$i
=0;
$i
<=300;
$i
++){
//写入文件,大概要3000微秒
usleep(3000);
echo
"写入文件{$i}\n"
;
}
}
function
task2(){
for
(
$i
=0;
$i
<=500;
$i
++){
//发送邮件给500名会员,大概3000微秒
usleep(3000);
echo
"发送邮件{$i}\n"
;
}
}
function
task3(){
for
(
$i
=0;
$i
<=100;
$i
++){
//模拟插入100条数据,大概3000微秒
usleep(3000);
echo
"插入数据{$i}\n"
;
}
}
task1();
task2();
task3();
|
这样,就实现了这3个功能了,然而,技术组长又说:
能不能改成交替运行呢?
就是说,写入文件一次之后,马上去发送一次邮件,然后再去插入一条数据
然后我改一改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
<?php
function
task1(
$i
)
{
//使用$i标识 写入文件,大概要3000微秒
if
(
$i
> 300) {
return
false;
//超过300不用写了
}
echo
"写入文件{$i}\n"
;
usleep(3000);
return
true;
}
function
task2(
$i
)
{
//使用$i标识 发送邮件,大概要3000微秒
if
(
$i
> 500) {
return
false;
//超过500不用发送了
}
echo
"发送邮件{$i}\n"
;
usleep(3000);
return
true;
}
function
task3(
$i
)
{
//使用$i标识 插入数据,大概要3000微秒
if
(
$i
> 100) {
return
false;
//超过100不用插入
}
echo
"插入数据{$i}\n"
;
usleep(3000);
return
true;
}
$i
= 0;
$task1Result
= true;
$task2Result
= true;
$task3Result
= true;
while
(true) {
$task1Result
&&
$task1Result
= task1(
$i
);
$task2Result
&&
$task2Result
= task2(
$i
);
$task3Result
&&
$task3Result
= task3(
$i
);
if
(
$task1Result
===false&&
$task2Result
===false&&
$task3Result
===false){
break
;
//全部任务完成,退出循环
}
$i
++;
}
|
运行一下:
代码1:
代码2:
确实是实现了任务交替执行,但是代码2明显让代码变的非常的难读,扩展性也很差,那么,有没有更好的方式实现这个功能呢?
这时候我们就必须借助yield了
首先,我们得封装一个任务类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
/**
* 任务对象
* 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
/**
* 任务调度
* 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
);
}
}
}
}
|
很好,我们已经有了一个调度类,还有了一个任务类,可以继续实现上面的功能了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
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();
|
除了上面的2个类,task函数和代码1不同的地方,就是多了个yield,那我们试着运行一下:
很好,我们已经实现了可以调度任务,进行任务交叉运行的功能了,这就是"协程"
协程可以将多个不同的任务交叉运行
二:协程与调度器的通信
我们在上面已经实现了一个协程封装了,但是任务和调度器缺少了通信,我们可以重新封装下,使协程当中能够获取当前的任务id,新增任务,以及杀死任务
先封装一下调用的封装:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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:
1
2
3
4
5
6
7
8
9
10
11
|
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函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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,去新增任务和杀死任务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
/**
* 传入一个生成器函数用于新增任务给调度器调用
* @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的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
/**
* 杀死一个任务
* @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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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/,
Swoole-4.1.0正式版发布, 主要改动及新特性:
+ PHP原生Redis、PDO、MySQLi轻松协程化, 使用Swoole\Runtime::enableCorotuine()即可将普通的同步阻塞Redis、PDO、MySQLi操作变为协程调度的异步非阻塞IO
+ 协程跟踪功能: 新增两个方法 Coroutine::listCoroutines()可遍历当前所有协程, Coroutine::getBackTrace($cid)可获取某个协程的函数调用栈
+ 支持在协程和Server中使用exit, 此时将会抛出可捕获的\Swoole\ExitException异常
+ 移除所有迭代器(table/connection/coroutine_list)的PCRE依赖限制
+ 新增http_compression配置项, 底层会自动判断客户端传入的Accept-Encoding选择合适的压缩方法, 支持GoogleBrotli压缩
+ 重构了底层Channel和协程Http客户端的C代码为C++协程模式, 解决历史遗留的异步时序问题, 稳定性大大提升
+ 更完整稳定的HTTP2支持和SSL处理
+ 增加open_websocket_close_frame配置, 可以在onMessage事件中接收close帧