Comment les générateurs et les coroutines sont-ils implémentés en PHP

不言
Libérer: 2023-04-03 10:24:02
original
1310 Les gens l'ont consulté

Cet article partage avec vous comment les générateurs et les coroutines sont implémentés en PHP. Le contenu est très bon. Les amis dans le besoin peuvent s'y référer. J'espère qu'il pourra aider tout le monde.

Parlons d'abord de bêtises

Depuis PHP 5.5, de nombreuses nouvelles fonctionnalités ont encore une fois donné un nouvel éclat à PHP, même si dans cet article il C'était quelque temps après la sortie de PHP 7 alpha 2, mais à cette époque le pays était encore dominé par PHP 5.3. Cependant, je pense que tôt ou tard, les nouvelles fonctionnalités deviendront plus importantes à mesure que les anciennes versions disparaîtront progressivement, surtout après la sortie de la version officielle de PHP 7. Par conséquent, le but de cet article est d'aider certains PHPers à comprendre quelque chose qu'ils n'ont jamais compris. compris avant quelque chose. Je prévois donc d'utiliser cet article comme début d'une série d'articles pour compléter les connaissances PHP dans le blog.

En fait, avant d'écrire cet article, je n'avais pas une compréhension relativement intuitive du générateur et de l'implémentation de coroutine de PHP basée sur cette fonctionnalité Principalement parce que mon niveau personnel n'est pas très élevé et que je suis un type. nouveau venu. La porte de PHPer. Ainsi, après avoir lu l'explication des coroutines sur le blog de Laruence il y a quelque temps (lien de référence : "Using Coroutines to Implement Cooperative Multitasking in PHP"), sur la base de ma compréhension personnelle de cet article, je me suis concentré sur celles qui sont les plus difficiles à comprendre. Le concept (y compris ma difficulté personnelle à comprendre ce concept) est expliqué d'une manière plus populaire. Bien sûr, puisque je viens tout juste de commencer à apprendre ce concept, il est inévitable que je fasse des erreurs inappropriées. J'espère que si vous le voyez, vous n'hésiterez pas à m'éclairer.

Tout commence par Iterator et Generator

Pour faciliter la compréhension des nouveaux développeurs, la moitié de cet article porte sur le Interface itérateur ( Iterator) et classes Generator, si vous comprenez déjà cela, vous pouvez l'ignorer directement.

Itération et itérateurs

Avant de comprendre la plupart des concepts de cet article, il est nécessaire de connaître l'itération et les itérateurs. En fait, tout le monde sait ce qu'est l'itération, mais je ne le sais pas (en réalité, je n'avais pas de compréhension systématique de ce concept auparavant). L'itération fait référence à l'exécution répétée d'un processus, et chaque exécution est appelée une itération. En fait, nous faisons souvent ce genre de chose, comme :

<?php
$mapping = [
  &#39;red&#39;  => &#39;#FF0000&#39;,
  &#39;green&#39; => &#39;#00FF00&#39;,
  &#39;blue&#39; => &#39;#0000FF&#39;
];
foreach ($mapping as $key => $value) {
  printf("key: %d - value: %s\n", $key, $value);
}
Copier après la connexion

Nous pouvons voir que foreach parcourt le tableau et affiche son contenu de manière itérative. Dans cette section, nous devons nous concentrer sur les tableaux. Bien que notre processus d'itération soit le bloc de code dans l'instruction foreach, le tableau $mapping change en fait à chaque itération, ce qui signifie qu'il y a également une itération à l'intérieur du tableau. Si nous considérons le tableau comme un objet, foreach appelle en fait une méthode de l'objet à chaque processus d'itération, permettant au tableau de changer (itérer) en interne, puis récupère les clés et les valeurs de l'objet tableau actuel via une autre méthode. . Un tel objet dont les données internes peuvent être parcourues en externe est un objet itérateur, et l'interface d'accès unifiée qu'il suit est l'interface itérateur (Iterator).

PHP fournit une interface d'itérateur unifiée. La documentation officielle PHP sur les itérateurs a une description plus détaillée et il est recommandé de la lire.

interface Iterator extends Traversable
{
  /**
   * 获取当前内部标量指向的元素的数据
   */
  public mixed current ( void )
  /**
   * 获取当前标量
   */
  public scalar key ( void )
  /**
   * 移动到下一个标量
   */
  public void next ( void )
  /**
   * 重置标量
   */
  public void rewind ( void )
  /**
   * 检查当前标量是否有效
   */
  public boolean valid ( void )
}
Copier après la connexion

Donnons un exemple pour implémenter un itérateur simple :

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;
  }
}
Copier après la connexion

Voyons l'effet de cet itérateur à travers le parcours foreach :

foreach (new Xrange(0, 10, 2) as $key => $value) {
  printf("%d %d\n", $key, $value);
}
Copier après la connexion

Sortie :

1 0
3 2
5 4
7 6
9 8
11 10

Jusqu'à présent, nous avons vu la mise en œuvre d’un itérateur. Certaines personnes seront très enthousiastes à l'idée de l'appliquer dans des projets réels après avoir découvert cette fonctionnalité, mais certaines personnes ne savent pas à quoi elle sert ? L'itérateur transforme simplement un objet ordinaire en un objet qui peut être parcouru. Dans certains cas, comme un objet StudentsContact, cet objet est utilisé pour traiter les informations de contact des étudiants via la méthode addStudent et obtenir tous les étudiants inscrits via getAllStudent. Tableau des coordonnées des étudiants. Dans le passé, nous utilisions StudentsContact::getAllStudent() pour obtenir un tableau puis parcourir le tableau, mais maintenant avec l'itérateur, tant que la classe hérite de cette interface, nous pouvons directement parcourir l'objet pour obtenir le tableau étudiant, et nous pouvons l'ajouter à la classe avant de l'obtenir. Les données de sortie sont traitées en interne.

Bien sûr, il a bien plus d’utilisations que cela, mais je ne m’embrouillerai pas trop ici. Il existe quelque chose de plus puissant sur cette base, des générateurs.

Générateur, Générateur

Bien que les itérateurs puissent être implémentés en héritant simplement de l'interface, cela reste très gênant. Après tout, nous devons définir une classe et tout implémenter. méthodes de l'interface, ce qui est très lourd. Dans certaines situations, nous avons besoin d’une approche plus simple. Les générateurs offrent un moyen plus simple d’implémenter une itération d’objet simple, avec une surcharge de performances et une complexité considérablement réduites par rapport à la définition d’une classe pour implémenter l’interface Iterator.

La documentation officielle de PHP dit ceci :

生成器允许你在 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);
}
Copier après la connexion

可能你已经发现了,这个例子的输出和我们前面在说迭代器的时候那个例子结果一样。实际上生成器生成的正是一个迭代器对象实例,该迭代器对象继承了 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(&#39;hello&#39;);
$printer->send(&#39;world&#39;);
Copier après la connexion

上述例子输出内容为:

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(&#39;hello&#39;);
printf("%d\n", $printer->current());
$printer->send(&#39;world&#39;);
printf("%d\n", $printer->current());
Copier après la connexion

输出内容如下:

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 即使不是最好,但也在努力变好的事实,对吧?

相关推荐:

PHP7新特性中抽象语法树(AST)的一些介绍

利用PHPExcel如何读取表格中内容

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal