Home > Article > Backend Development > How are generators and coroutines implemented in PHP
This article shares with you how generators and coroutines are implemented in PHP. The content is very good. Friends in need can refer to it. I hope it can help everyone.
Let’s talk some nonsense first
Since PHP 5.5, many new features have once again given PHP a new luster, although in this article It was some time after the release of PHP 7 alpha 2, but at this time the country was still dominated by PHP 5.3. However, I think that sooner or later, new features will become more important as the old versions gradually disappear, especially after the official version of PHP 7 comes out. Therefore, the purpose of this article is to help some PHPers understand something they have never understood before. s things. So I plan to use this article as the beginning of a series of articles to complete the PHP knowledge in the blog.
In fact, before writing this article, I did not have a relatively intuitive understanding of the generator and the coroutine implementation of PHP extended based on this feature. Mainly because my personal level is not very high and I am a typical newcomer. The door to PHPer. So after reading the explanation of coroutines in laruence's blog some time ago (reference link: "Using Coroutines to Implement Cooperative Multitasking in PHP"), based on my personal understanding of this article, I focused on those that are more difficult to understand. The concept (including my personal difficulty in understanding this concept) is explained in a more popular way. Of course, since I have just started to learn this concept, it is inevitable that I will make some inappropriate mistakes. I hope that if you see it, please feel free to enlighten me.
Everything starts with Iterator and Generator
In order to facilitate the understanding of new entry-level developers, half of this article is about the iterator interface ( Iterator) and Generator classes, if you already understand this, you can skip it directly.
Iteration and iterators
Before understanding most of the concepts in this article, it is necessary to know iteration and iterators. In fact, everyone knows what iteration is, but I don't know (really, I didn't have a systematic understanding of this concept before). Iteration refers to executing a process repeatedly, and each execution is called an iteration. In fact, we often do this kind of thing, such as:
<?php $mapping = [ 'red' => '#FF0000', 'green' => '#00FF00', 'blue' => '#0000FF' ]; foreach ($mapping as $key => $value) { printf("key: %d - value: %s\n", $key, $value); }
We can see that foreach
traverses the array and iteratively outputs its contents. In this section, the focus we need to focus on is arrays. Although our iteration process is the code block in the foreach
statement, the array $mapping
actually changes in each iteration, which means that there is also an iteration inside the array. If we regard the array as an object, foreach actually calls a method of the object during each iteration process, allowing the array to change (iterate) internally, and then retrieves the keys and values of the current array object through another method. . Such an object whose internal data can be traversed externally is an iterator object, and the unified access interface it follows is the iterator interface (Iterator
).
PHP provides a unified iterator interface. The official PHP documentation about iterators has a more detailed description, and it is recommended to read it.
interface Iterator extends Traversable { /** * 获取当前内部标量指向的元素的数据 */ public mixed current ( void ) /** * 获取当前标量 */ public scalar key ( void ) /** * 移动到下一个标量 */ public void next ( void ) /** * 重置标量 */ public void rewind ( void ) /** * 检查当前标量是否有效 */ public boolean valid ( void ) }
Let’s give an example to implement a simple iterator:
class Xrange implements Iterator { protected $start; protected $limit; protected $step; protected $i; public function __construct($start, $limit, $step = 0) { $this->start = $start; $this->limit = $limit; $this->step = $step; } public function rewind() { $this->i = $this->start; } public function next() { $this->i += $this->step; } public function current() { return $this->i; } public function key() { return $this->i + 1; } public function valid() { return $this->i <= $this->limit; } }
Let’s see the effect of this iterator through foreach traversal:
foreach (new Xrange(0, 10, 2) as $key => $value) { printf("%d %d\n", $key, $value); }
Output:
1 0
3 2
5 4
7 6
9 8
11 10
So far we have seen a Iterator implementation. Some people will be very excited to apply it in actual projects after learning about this feature, but some people are confused about what use it is? The iterator just turns an ordinary object into an object that can be traversed. In some cases, such as an object StudentsContact, this object is used to process student contact information. Register students through the addStudent method, and obtain all registered students through getAllStudent. Array of student contact information. In the past, we used StudentsContact::getAllStudent() to get an array and then traverse the array, but now with the iterator, as long as the class inherits this interface, we can directly traverse the object to get the student array, and we can add it to the class before getting it. The output data is processed internally.
Of course, it has far more uses than this, but I won’t get too entangled here. There is something more powerful on this basis, generators.
Generator, Generator
Although iterators can be implemented by simply inheriting the interface, it is still very troublesome. After all, we need to define a class and implement all methods of the interface. , which is very cumbersome. In some situations we need a simpler approach. Generators provide an easier way to implement simple object iteration, with greatly reduced performance overhead and complexity compared to defining a class to implement the Iterator interface.
PHP official documentation says this:
生成器允许你在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组, 那会使你的内存达到上限,或者会占据可观的处理时间。相反,你可以写一个生成器函数,就像一个普通的自定义函数一样, 和普通函数只返回一次不同的是, 生成器可以根据需要 yield 多次,以便生成需要迭代的值。
一个简单的例子就是使用生成器来重新实现 range() 函数。 标准的 range() 函数需要在内存中生成一个数组包含每一个在它范围内的值,然后返回该数组, 结果就是会产生多个很大的数组。 比如,调用 range(0, 1000000) 将导致内存占用超过 100 MB。
做为一种替代方法, 我们可以实现一个 xrange() 生成器, 只需要足够的内存来创建 Iterator 对象并在内部跟踪生成器的当前状态,这样只需要不到1K字节的内存。
官方文档给了上文对应的例子,我们在此简化了一下:
function xrange($start, $limit, $step = 1) { for ($i = $start; $i <= $limit; $i += $step) { yield $i + 1 => $i; // 关键字 yield 表明这是一个 generator } } // 我们可以这样调用 foreach (xrange(0, 10, 2) as $key => $value) { printf("%d %d\n", $key, $value); }
可能你已经发现了,这个例子的输出和我们前面在说迭代器的时候那个例子结果一样。实际上生成器生成的正是一个迭代器对象实例,该迭代器对象继承了 Iterator 接口,同时也包含了生成器对象自有的接口,具体可以参考 Generator
类的定义。
当一个生成器被调用的时候,它返回一个可以被遍历的对象.当你遍历这个对象的时候(例如通过一个foreach循环),PHP 将会在每次需要值的时候调用生成器函数,并在产生一个值之后保存生成器的状态,这样它就可以在需要产生下一个值的时候恢复调用状态。
一旦不再需要产生更多的值,生成器函数可以简单退出,而调用生成器的代码还可以继续执行,就像一个数组已经被遍历完了。
我们需要注意的关键是 yield
,这是生成器的关键。我们通过上面例子,可以看得出,yield
会将当前一个值传递给 foreach
,换句话说,foreach
每一次迭代过程都会从 yield 处取一个值,直到整个遍历过程不再存在 yield 为止的时候,遍历结束。
我们也可以发现,yield 和 return 都会返回值,但区别在于一个 return 是返回既定结果,一次返回完毕就不再返回新的结果,而 yield 是不断产出直到无法产出为止。
实际上存在 yield 的函数返回值返回的是一个 Generator
对象(这个对象不能手动通过 new 实例化),该对象实现了 Iterator
接口。那么 Generator
自身有什么独特之处?继续看:
yield
字面上解释,yield 代表着让位、让行。正是这个让行使得通过 yield 实现协程变得可能。
生成器函数的核心是 yield 关键字。它最简单的调用形式看起来像一个 return 申明,不同之处在于普通 return 会返回值并终止函数的执行,而 yield 会返回一个值给循环调用此生成器的代码并且只是暂停执行生成器函数。
yield 和 return 的区别,前者是暂停当前过程的执行并返回值,而后者是中断当前过程并返回值。暂停当前过程,意味着将处理权转交由上一级继续进行,直至上一级再次调用被暂停的过程,该过程则会从上一次暂停的位置继续执行。这像是什么呢?如果读者在读本篇文章之前已经在鸟哥的文章中粗略看过,应该知道这很像是一个操作系统的进程调度管理,多个进程在一个 CPU 核心上执行,在系统调度下每一个进程执行一段指令就被暂停,切换到下一个进程,这样看起来就像是同时在执行多个任务。
但仅仅是如此还远远不够,yield 更重要的特性是除了可以返回一个值以外,还能够接收一个值!
function printer() { while (true) { printf("receive: %s\n", yield); } } $printer = printer(); $printer->send('hello'); $printer->send('world');
上述例子输出内容为:
receive: hello
receive: world
参考 PHP 官方中文文档:生成器 对象 我们可以得知 Generator 对象除了实现 Iterator 接口中的必要方法以外,还有一个
send
方法,这个方法就是向 yield 语句处传递一个值,同时从 yied 语句处继续执行,直至再次遇到 yield 后控制权回到外部。
我们通过之前也了解了一个问题,yield 可以在其位置中断并返回一个值,那么能不能同时进行 接收 和 返回 呢?当然,这可是实现协程的根本。我们对上述代码做出修改:
<?php function printer() { $i = 0; while (true) { printf("receive: %s\n", (yield ++$i)); } } $printer = printer(); printf("%d\n", $printer->current()); $printer->send('hello'); printf("%d\n", $printer->current()); $printer->send('world'); printf("%d\n", $printer->current());
输出内容如下:
1
receive: hello
2
receive: world
3
current
方法是迭代器( Iterator
)接口必要的方法,foreach
语句每一次迭代都会通过其获取当前值,而后调用迭代器的 next
方法。我们为了使程序不会无限执行,手动调用 current 方法获取值。
上述例子已经足以表示 yield 在那一个位置作为双向传输的 工具,已具备实现协程的条件。
协程
这一部分我不打算长篇大论,本文开头已经给出了鸟哥博客中更为完善的文章,本文的目的是出于补充对 Generator 的细节。
我们要知道,对于单核处理器,多任务的执行原理是让每一个任务执行一段时间,然后中断、让另一个任务执行然后在中断后执行下一个,如此反复。由于其执行切换速度很快,让外部认为多个任务实际上是 “并行” 的。
鸟哥那篇文章这么说道:
多任务协作这个术语中的 “协作” 很好的说明了如何进行这种切换的:它要求当前正在运行的任务自动把控制传回给调度器,这样就可以运行其他任务了。这与 “抢占” 多任务相反, 抢占多任务是这样的:调度器可以中断运行了一段时间的任务, 不管它喜欢还是不喜欢。协作多任务在 Windows 的早期版本 (windows95) 和 Mac OS 中有使用, 不过它们后来都切换到使用抢先多任务了。理由相当明确:如果你依靠程序自动交出控制的话,那么一些恶意的程序将很容易占用整个CPU,不与其他任务共享。
我们结合之前的例子,可以发现,yield
作为可以让一段任务自身中断,然后回到外部继续执行。利用这个特性可以实现多任务调度的功能,配合 yield
的双向通讯功能,以实现任务和调度器之间进行通信。
这样的功能对于读写和操作 Stream 资源时尤为重要,我们可以极大的提高程序对于并发流资源的处理能力,比如实现 tcp server。
总结
PHP 自 5.4 到如今愈发稳定的 PHP 7,可以看到许多的新特性令这门语言愈发强大和完善,逐渐从纯粹的 Web 语言变得有着更为广泛的适用面,作为一枚 PHPer 的确不应当止步不前,我们依然有很多的东西需要不断学习和加强。
虽然 “PHP 是世界上最好的语言” 这句话只是个调侃,但不可否认 PHP 即使不是最好,但也在努力变好的事实,对吧?
相关推荐:
The above is the detailed content of How are generators and coroutines implemented in PHP. For more information, please follow other related articles on the PHP Chinese website!