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