diff --git a/_posts/编程/PHP/coroutine/yield-coroutine.md b/_posts/编程/PHP/coroutine/yield-coroutine.md new file mode 100644 index 0000000..f69f2c7 --- /dev/null +++ b/_posts/编程/PHP/coroutine/yield-coroutine.md @@ -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生产流程。 + +这种暂停和恢复执行的能力在处理大量数据、遍历复杂的数据结构、实现惰性计算等场景下可以节省大量内存和提高性能。原因它只在需要时才生成值,而不是一次性生成整个序列! + + + +## 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!