PHP 的原生协程 (yield/yield from/fiber) 到底有什么用,或者说后续 PHP9 如何发展?

PHP 的原生协程 (yield/yield from/fiber) 到底有什么用,或者说后续如何发展?

Explore PHP Coroutine

  • Title: PHP 的原生协程 (yield/yield from/fiber) 到底有什么用,或者说后续如何发展?
  • Tag: PHPCoroutineyieldfiber
  • Author: Tacks
  • Create-Date: 2023-11-22
  • Update-Date: 2023-11-22

0x1. 👓 大话一下(进程、线程、协程)

  • 纸带打孔时期

早期打孔纸带时期,都是人工来介入,一边是程序员进行打孔,一边是运输员把纸带送入传输机中,计算机同时只能载入一个纸带(内存中单个纸带作业)进行运算(单个作业),然后产生结果以纸带的形式输出。

🤯:问题是这样手速太慢了,而且每次只能载入一个纸带,CPU 大佬就是说非常闲,于是人们就想办法预先多加载纸带,来加快人工录入。

😎:于是引入 『批处理』 脱机输入/输出技术,(利用外围机将纸带数据存入磁盘)

  • 单道批处理时期

『单道批处理时期』,引入磁带作为外部介质,通过外围机可以将多条纸带任务预先载入磁带,然后配合系统的 监督程序 ,让这些作业可以依次加载到内存中,但是 CPU 还是一个作业一个作业执行,毕竟单核 CPU 也不能并行。

🤯:磁带录入速度比纸带快,一定程度上让人工的效率提高了,但内存中还是只能有一道作业(单道),CPU 结束了一个作业,内存中才能再次载入新的作业,然后交给 CPU 去执行;

🤖:CPU 说,你们 IO 太慢了,加载个作业,我都睡上一觉了,哈哈哈哈哈 ~

😎:既然单道不行,那就多道,想办法在内存中存放多个作业,反正就把作业都给你 CPU 跑, 于是引入了 『进程』 ,就是想办法隔离两个作业,又不会互相影响。

  • 多道批处理时期

『多道批处理时期』,有了『操作系统』、『进程』、『中断』这些概念,操作系统可以批量将磁带里面的作业进行IO操作,作业调度器会从这一批作业中,选择若干个读取到内存中,给他们分配内存空间,让他们共享 CPU和系统资源,调度某个作业拥有 CPU 控制权可以执行,当发生硬件中断,例如IO完成、时钟中断之类的,或者软件中断,系统调用、异常中断等,操作系统就会保存现场,切换到中断程序,然后调度另一个作业。从而大幅度提高 CPU 的利用率,宏观上看是多个作业并行,微观上还是串行执行。

🤯:虽然多道作业并发执行,但是如果某个作业很耗时,或者陷入死循环,如何得到对应的结果

🤖:我 CPU 处理程序,就是认真处理,你不主动问我,我为啥告诉你?

😎:那我就让你共享给大家一起用(分时技术),不要运输员传输了,程序员直接跟你交互 (人机交互)

  • 分时系统时期

『分时系统时期』,引入『分时技术』,把 CPU运行时间,分成很小的时间片,采用时间片轮转的抢占调度策略,分配给各个用户/任务使用。如果某个任务在时间片内无法完成,就会由操作系统暂停运行,把 CPU 让给其他的作业。这允许了多个终端用户同时使用一台计算机,并且可以进行『人机交互』,大大提高交互的友好型,好像共享了同一台计算机。另外调度策略除了 『时间片轮询调度』 ,还有 『最高优先级队列调度』,按照进程的优先级来执行,以及还有 『多级反馈队列调度』,就是优先级高的时间片越短,优先级低的时间片可以多分配一些。

  • 多线程出现

线程』的出现,CPU 调度也选择粒度更小的线程来执行任务,CPU 在多个线程来回切换,多个线程共享了进程内存资源,但是也有自己的寄存器和栈来进行保存上下文信息。多线程下,任务分工也明确了,进程负责分配和管理系统资源,线程进行 CPU 调度运算,以这样的方式来完成 『抢占式』 的多任务。

🤯:虽然多线程有了,但是 CPU 还是单个,同一时刻还是单线程再跑,还能咋优化一下

🤖:我天天忙个不停,你又要折腾我了 ☠

😎:如何让 CPU 分割一下?

  • 超线程出现

超线程』是 Intel 推出的一项技术,它可以让单个物理处理器核心模拟成两个逻辑核心,而逻辑CPU在操作系统上感知和物理CPU是一样的,从而提高处理器的 『并行』 处理能力。

🤯:超线程让 CPU 一个变俩,障眼法而已,也就是两个任务并行,后期可以多核吗

🤖:终于想通了,把我串起来,多核不就好了

😎:随着半导体技术的发展,『多核处理器』 也来了 ,可以把 多个CPU 核心 集成到单个集成电路芯片中,真不错

  • 协程的出现

早期协程的出现还没有线程,主要思想是控制流的主动让出和恢复机制。后来有了线程,出现了内核态线程和用户态线程的说法。 进程和线程都是操作系统层面的概念,切换成本都比较高,用户态线程切换代价比内核态线程小,此时协程更多被理解为用户态线程,成为了轻量级的多任务并发模型。于是在后来的编程语言如 LuaPythonJavascriptGoKotlinPHPJavaC++Rust 都有类似的协程的语法糖实现。

主流编程语言的协程方案

  • PHP
    • yield / fiber
  • Go
    • goroutine
  • C++
    • co_yield / co_await / co_return
  • Javascript/NodeJs
    • yield / async / await / Promise
  • Python
    • yield / async / await

0x2. 🎡 协程的好处

  • 协程的本质是优化 IO 密集型下的 CPU 利用率

协程的本质,是控制流的主动让出和恢复机制; 你猜为什么要让出控制流,那肯定是当某个协程用不到 CPU 了,此时他主动让出,是不是就能让别的协程获取;

协程的性能,最主要的是体现在 IO 上;通常是需要 IO 操作,会进行阻塞,也就是我们常说的 IO 密集型,此时协程主动让出,让其他协程利用 CPU;

  • 协程往往和异步IO结合起来

协程是轻量级用户态线程,调度是非抢占式的,需要当前协程主动切换到其他协程。通常来说协程框架都是 1:N ,也就是一个线程对应多个协程,协程池里面会有一个调度器,当某个协程说我被阻塞了,就会告知调度器去寻找最需要 CPU 的协程完成执行。

但但但是你要知道 Linux 看到的就是线程,它并不知道协程。所以说此时协程内部发生 IO 阻塞,会导致线程挂起,因为也无法切换成其他协程执行。所以通常 协程要配合非阻塞 IO,来确保线程不会因为协程阻塞而导致挂起。所以所以,协程框架要做的就是这个事情,封装一层,或者是 Hook 一层,让原本看似阻塞 IO 的操作变成异步 IO 的操作。

0x3. 🐘 PHP 的原生协程发展

大概历史如下:

  • 2012-06-05 PHP5 引入生成器 Generator ,利用 yield 语法糖优化生成器,只要包含 yield 关键字的函数返回的都是 Generator
  • 2015-02-18 PHP7 引入生成器返回表达式 , Generator 类,提供 getReturn() 方法获取迭代后的返回值
  • 2015-03-01 PHP7 引入生成器委托 ,利用 yield from 语法糖减少嵌套生成器函数的编写,使生成器函数的逻辑更清晰和可读
  • 2021-03-08 PHP8 引入纤程 fiber,内置 FiberReflectionFiber 类,允许在任何点将异步代码执行无缝集成到同步代码中

大概理解:

  • yield 来解决生成器的实现
  • 生成器对于迭代和协作多任务处理来说无疑是有用
  • 生成器函数的定义特征是它们支持暂停执行以便稍后恢复。此功能为应用程序提供了一种实现异步和并发架构的机制
  • PHP 单线程语言通过简单的用户态任务调度系统,交错生成器成为并发处理任务的轻量级执行线程
  • fiber 可以视为 yield 的改进, 是对 PHP 原生异步功能的一个很好的补充,从无到有
  • fiber 更关注 reactphp/amphp 这类异步框架的 API 统一,说白了就是给框架开发人员用的

0x4. 👻 PHP 为什么引入协程

众所周知 PHP 的线程方案基本没有应用生产环境,大多数还是多进程 php-fpm 的解决方案,当然官方也是推崇这个,至于基于 php-cli 的应用架构,这里暂且先不说。

  • 引入 yield ,开始只是因为生成器

PHP 一开始其实就参考了 Python 的 yield 用法,利用生成器来实现一种简单的迭代器,虽然有现成的 Iterator 迭代器接口,但是你要用吧,就需要自己实现一个类,所以 Generator 的产生,也是提供了一个简单的模板,并且用 yield 语法糖就能轻松创建一个 生成器类,(注意Generator 并不能手动 new 来实例化)。 提出这个需求的作者,也是希望可以帮忙解决一个测试需求,需要把一堆文件全部加载到内存中,然后再去依次遍历,所以就引出了生成器的语法糖。

  • 引入 yield from ,带来了 有栈协程

包含 yield 的函数就是一个 Generator ,但无法简单地互相嵌套组合,或者自己利用栈来存储,封装一层。好在后期也出现了 yield from,毕竟编程语言层面出现的语法糖,往往效率也比较高,比较好用。

就类似,Func() 里面有多个 func1()func2() yield 函数,组合成一个大的 Generator,由 PHP 负责进行调度, 不用再自己维护调度栈。 每一个 yield from 会自动调用 GeneratorgetReturn() 方法作为返回值。

  • PHP 没有多线程加锁的问题,协程也不用加锁

利用 yield 的协程,往往也都是单进程单线程内的,多个协程进行调度执行。

  • PHP 下的原生协程,也无法利用多核的特点

相比之下 go 下的goroutine,M:N 的协程解决方案确实是厉害。

  • PHP 引入的 fiber ,确实带来了一些希望

fiber 可以起到控制协程暂停和恢复、双向通信的作用,完全可以替代 yield 。相对于 yield 构造的 Generator , fiber 则是提供更高性能的方案,由 c 大佬出马,保存协程的状态和协程的调度执行,这样协程之间切换非常轻量级,看官方说改变大约 20 个指针的值。

  • PHP fiber 有希望,但不多

fiber 必须结合异步 IO、事件循环,来防止阻塞代码影响到整个进程,这让一开始就习惯用 PHP 做同步开发的来说,确实不习惯,而且PHP 下大部分的函数都是同步阻塞函数,如果在协程内使用了某个阻塞函数,则会导致这个线程陷入阻塞。

  • PHP 第三方类库 ReactPHP / AmPHP , 异步 + 协程才能发挥出性能

ReactPHP 参考了 JS 的 Promise, 貌似也是要试图把一些操作全部替换成异步承诺,另外依靠底层的扩展如 libevent ,也有 php 兜底的 stream_select 实现 IO 多路复用。

  • 另外 Swoole 4.0 称自己为协程框架 ,走的一条特别的路线

Swoole 底层基于 C/C++ 的 PHP 扩展,那就需要自己实现调度器,自己保存自己协程栈,协程上下文用于切换,另外是采用 Hook 原生 PHP 函数,让原来的同步 IO 的代码变成可以协程调度的异步 IO 。 所以后面的 fiber , Swoole 也用不上,反而是像 reactphp/amphp 这样的基于事件循环框架可以朝 fiber 这个方向发展异步协程 。

0x5. 🐱‍🏍 所以 PHP 的协程之路有啥方向和前景吗?

大概看了一下 PHP 下的原生协程,怎么说,不痛不痒。可能 yield 生成器,在处理大内存量数据的时候,使用迭代器来解决也不错。但是新的这个 fiber ,目前也没有在生产中使用到。看了一圈发现确实 Go 的原生 goroutine 确实屌,算是并行运行的有栈协程,也能用到多核。如果本身项目就是 Swoole,也可以用 Swoole\Coroutine\run(),来玩一玩协程。 NodeJs 下的协程是因为 JS 单线程,天生支持异步得以发展起来。C++20 也紧跟潮流出了 co_yield 。话说回来,PHP的原生协程,将来会如何发展呢, PHP8 新的理念,都出的差不多了,啥时候规划 PHP9

明天我们吃什么 悲哀藏在现实中 Tacks
本帖已被设为精华帖!
本帖由 MArtian 于 5个月前 加精
Jyunwaa
最佳答案

赞同楼主的观点,GO的协程目前可以算是最厉害的,M:N模型自动调度充分利用多核,PHP的协程框架不管是React还是Amphp,我的评价是难用,至少我不会考虑用它。Swoole/Swow走了另一条路,Hook原生函数,现在最新版本又支持了pdo_pgsql/pdo_odbc,在使用上已经很接近GO了,而且基本不会对PHP的原本语法造成破坏(React和Amphp需要使用单独的协程客户端),毫不夸张地说,Swoole/Swow就是最佳的PHP协程解决方案。不过PHP程序员不应该局限于PHP,应该往其它领域扩展,PHP8.3的更新一言难尽,它之所以落寞内部原因也不可忽视。

大胆预测一下PHP9的发展方向:①继续往向强类型、静态编译型方向靠拢,优化JIT,进一步提升计算密集性能;②集成重要的外部扩展;③提供完整的协程方案,实现开箱即用的原生协程;④向AI领域靠拢(PHP这么多年竟然都没有矩阵运算,震惊,但是PHP的未来规划上是有支持这些东西的计划的)。

4个月前 评论
讨论数量: 7

没啥用,这些不疼不痒的东西搞100个也阻止不了PHP在中国的没落,SWOOLE给PHP续了下命,但是毕竟不是官方搞的东西

5个月前 评论
lovewei 5个月前
Tacks (楼主) 5个月前

只是fiber实现太底层了,和yield一样没有调度和控制实现。这些在cli 下才有最佳表现,fpm环境让PHP好多优秀扩展黯然失色,如pcntl,event。

swoole 目前完成度,我觉得已经达到应用层标准了。基本上实现开箱即用.

5个月前 评论
Tacks (楼主) 5个月前
Jyunwaa

赞同楼主的观点,GO的协程目前可以算是最厉害的,M:N模型自动调度充分利用多核,PHP的协程框架不管是React还是Amphp,我的评价是难用,至少我不会考虑用它。Swoole/Swow走了另一条路,Hook原生函数,现在最新版本又支持了pdo_pgsql/pdo_odbc,在使用上已经很接近GO了,而且基本不会对PHP的原本语法造成破坏(React和Amphp需要使用单独的协程客户端),毫不夸张地说,Swoole/Swow就是最佳的PHP协程解决方案。不过PHP程序员不应该局限于PHP,应该往其它领域扩展,PHP8.3的更新一言难尽,它之所以落寞内部原因也不可忽视。

大胆预测一下PHP9的发展方向:①继续往向强类型、静态编译型方向靠拢,优化JIT,进一步提升计算密集性能;②集成重要的外部扩展;③提供完整的协程方案,实现开箱即用的原生协程;④向AI领域靠拢(PHP这么多年竟然都没有矩阵运算,震惊,但是PHP的未来规划上是有支持这些东西的计划的)。

4个月前 评论

:pushpin:插个眼,到时候 PHP9 来了踢你! @Jyunwaa

  • 【1】 类型声明定义无可厚非以及基本有了,但强类型玄乎,静态编译有点难估计,毕竟本质还是动态语言,运行时推导出来的变量类型,不过对静态代码分析器,如PHPStan/Psalm这类工具肯定是友好的。JIT的引入确实CPU密集型友好,但是在传统WEB开发层面,emmm,暂且想不到哪种场景下优化效率高,不过有总比没有强 ( :wolf: 。
  • 【2】扩展一直是PHP的拿手绝活,核心扩展目前六七十个,看后面会不会把 swow 加进去,再出一些有意思的扩展,毕竟 PHP还是C程序员开发,应该啥都能搞吧,哈哈哈哈
  • 【3】看布局 fiber 这样,还真是希望能出一个比较优秀的 协程解决方案。
  • 【4】PHP 关于机器学习,矩阵之类的计算,我记得貌似有个 composer 包可以做一些 composer require php-ai/php-ml ,JIT 感觉对这种应该是有一定加速或者优化效果。

貌似 php8 开始 鸟哥也走了,核心开发者 nikic 也走了貌似研究 LLVM, 不知道后续 PHP 还是想守住 Web 开发,还是拓展一下其他的方向,能够活活水。

4个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!