🧬PHP的yield到协程直观的理解

This commit is contained in:
zyimm 2023-11-01 17:45:26 +08:00
parent 25a36d99dc
commit f93e975f9c

View File

@ -0,0 +1,116 @@
---
title: 🧬PHP的yield到协程直观的理解
date: 2023-11-01
tags: PHP,协程
---
在PHP8.1未发布`fiber`之前,你可能会听到一些前辈说"PHP的`yield`就是协程",但是官方文档对于`yield`是如下解释:
> `yield`(生成器)提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现 Iterator 接口的方式,性能开销和复杂性大大降低。
> 生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值。
官方的解释自然是正确和科学的,可是你的疑问是这个和协程有什么直接关系呢?
所以我将从生成器概念讲起到`yield`关键字使用,再到协程概念。或许你会从我的讲解得到一些解惑,当然如有不正之处,欢迎指点!
## 生成器的概念
在计算机科学中,生成器是一种特殊的函数,它可以按需生成一系列值,而不需要一次性生成整个序列。比较直观的例子就是汽车厂家是按照订单生产汽车,来一个订单生产一辆。而不是先大批量生产之后再分发销售。这样的好处是厂家节省资源,避免浪费。
生成器还有一个重要特性是它能够暂停和恢复执行还是以汽车厂家生产汽车为例子假设厂家接受了两个汽车订单一单生产轿车一单生产suv但是生产suv进行一半流程中发现部分零件需要等待采购。厂家自然会暂停生产suv流程转而去生产轿车避免生产线停产等待从而浪费资源。待零件采购回来再恢复suv生产流程。
这种暂停和恢复执行的能力在处理大量数据、遍历复杂的数据结构、实现惰性计算等场景下可以节省大量内存和提高性能。原因它只在需要时才生成值,而不是一次性生成整个序列!
<!--more-->
## yield的使用
在PHP想使用生成器需要使用`yield`关键字进行定义。每次遇到 yield 关键字时函数会将当前状态保存下来并返回一个值给调用者。PHP在此外部实现`Iterator`接口的方式,这样我们可以通过`Iterator`接口约定来调度&使用。
目前常用的有如下方法:
1. valid() 判断生成器是否可用
2. current() 返回当前yield表达式如果生成器已关闭则返回null
3. send() 设置yield表达式的返回值并恢复生成器
```php
//这边语法需要PHP7.0 以上版本运行
function gen(): Generator
{
$result = yield 'yield1';
var_dump('yield1:'.$result);
$result = yield 'yield2';
var_dump('yield2:'.$result);
$result = yield 'yield3';
var_dump('yield3:'.$result);
}
$gen = gen();
var_dump($gen->current()); // 默认返回第一个yield表达式的内容:"yield1"
var_dump($gen->send(1)); // 恢复yield1设置yield1表达式返回内容为1 返回下一个yield表达式
var_dump($gen->send(2));// 恢复yield2设置yield2表达式返回内容为2 返回下一个yield表达式
var_dump($gen->send(3));// 恢复yield3设置yield2表达式返回内容为2 返回下一个yield表达式
var_dump($gen->valid()); //返回false 因为上一步所有yield已经全部执行完了生成器已关闭
```
**因为生成器按需生成一系列值所以不可逆变也是恢复yield1之后只能往下走**
到此这一步在回味官方的例子是否加深理解?
## 协程概念
协程Coroutine是一种轻量级的并发执行模型可以在一个线程内实现多个任务的交替执行。
1. 并发执行:协程是一种轻量级的并发执行模型,可以在一个线程内实现多个任务的交替执行。与传统的多线程或多进程模型相比,协程的切换开销更小,并且可以充分利用单个线程的资源。
2. 非抢占式调度:协程采用非抢占式调度,即任务之间主动让出执行权,而不是由调度器强制切换。这种方式可以更好地控制任务的执行顺序和协作方式,提高效率和灵活性。
3. 状态保存和恢复:协程能够在执行过程中保存当前的状态,并在需要时恢复到之前的状态。这使得协程可以在执行过程中暂停和恢复,实现更灵活的任务切换和状态管理。
4. 协程通信:协程之间可以进行通信和数据交换,实现协作式的任务处理。协程通信可以通过共享变量、消息传递等方式实现,用于协调任务之间的交互和协作。
**举个例子:**
煮泡面,假设我们把煮泡面,分为两个任务:
1. 任务一 拆方便面包,放入碗中 耗时1min
2. 任务二 烧开水 耗时:5min
如果按照常规需要6min之后我们才能开始泡面。如果换成协程那么执行逻辑应该如下
1. 拆方便面包,放入碗中 这个一步进行5s 暂停
2. 切换到烧开水这个一步进行5s 暂停回到1
3. 上述1和2两个任务不停来回切换直到任务完成计算内部切换很快所以开起来两个任务在并发执行
按照这样逻辑回到计算机层面上面,协程的并发执行,非抢占式调度,状态保存和恢复是否更好的理解。同样也能解释在协程里面不能出现阻塞进程操作,否则协程退化成传统同步阻塞一样。
再者现在cpu大多是多核意味着同一个时刻可以处理多个任务那么协程的优势更加明显
## 还需很多
所以开始讲的生成器具有特性+加上一个任务调度器不就实现基本的协程的吗?
1. 生成器,负责任务生成暂停和切换
2. 任务调度器,负责任务切换和状态管理以及任务之间的通信和数据交换
鸟哥一篇博文[在PHP中使用协程实现多任务调度](https://www.laruence.com/2015/05/28/3038.html)基于生成器附加任务调度器实现异步非阻塞tcp服务器整体文章篇幅很大我还是建议大家可以品味一下任务调度器那部分实现细节
## fiber与yield(Generator)比较
**fiber** 是在 PHP8.1 中引入的扩展,它提供了一种更高效和灵活的协程模型,可以实现更细粒度的协程调度和协程间的通信。
生成器没有栈所以鸟哥博文中设计了任务和调度器还要设计它们之前如何通信原文使用yield关键字配合send来通信
Fiber是拥有自己的调用栈并允许内部任意位置暂停也不需要指定返回类型。这就比用 yield 一样需要返回一个 Generator 实例,清晰明了多了。
Fiber可以使用 Fiber::resume() 传递任意值、或者使用 Fiber::throw() 向纤程抛出一个异常以恢复运行。
所以与 yield 相比fiber 具有以下区别和优势:
1. 更高效的协程调度fiber 使用底层的协程调度机制,可以在更细粒度的级别上进行协程切换,从而提供更高效的协程调度和执行。
2. 更灵活的协程控制fiber 允许在协程之间手动进行切换,而不需要依赖生成器函数中的 yield 关键字。这使得协程的控制更加灵活,可以根据需要在任何时候切换协程。
3. 更强大的协程通信fiber 提供了更强大的协程通信机制,可以在协程之间传递数据和消息,实现更复杂的协程间交互和协作。
但是截止本博文发稿之前fiber目前还远远达不到类似swoole的协程开箱即用的程度调度器实现仍然需要开发者自己实现如果想快速简单使用协程建议使用swoole