>  기사  >  백엔드 개발  >  PHP에서 생성기와 코루틴은 어떻게 구현됩니까?

PHP에서 생성기와 코루틴은 어떻게 구현됩니까?

不言
不言원래의
2018-07-21 13:11:001292검색

이 글은 PHP에서 제너레이터와 코루틴을 구현하는 방법을 공유합니다. 내용이 매우 좋습니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

말도 안 되는 얘기부터 먼저 해볼까요

PHP 5.5, 많은 새로운 기능으로 인해 PHP가 다시 한 번 새 것처럼 보입니다. 이 글이 작성된 시점은 PHP 7 알파 2가 출시된 지 어느 정도 시간이 지난 시점이지만, 현재 국내에서는 여전히 PHP 5.3이 대세를 이루고 있습니다. 그러나 나는 특히 PHP 7의 공식 버전이 나온 후에는 이전 버전이 점차 사라지기 때문에 조만간 새로운 기능이 더 중요해질 것이라고 생각합니다. 따라서 이 기사의 목적은 일부 PHP 사용자가 한 번도 경험해 보지 못한 기능을 이해하도록 돕는 것입니다. 뭔가 전에 이해. 그래서 나는 이 기사를 블로그의 PHP 지식을 완성하기 위한 일련의 기사의 시작으로 사용할 계획입니다.

사실 이 글을 쓰기 전에는 제너레이터와 이 기능을 기반으로 한 PHP 확장 코루틴 구현에 대해 상대적으로 직관적인 이해가 없었습니다. 나는 PHPer를 처음 사용합니다. 그래서 얼마 전 laruence의 블로그에서 코루틴에 대한 설명을 읽은 후(참조 링크: "코루틴을 사용하여 PHP에서 협력적 멀티태스킹 구현") 이 기사에 대한 개인적인 이해를 바탕으로 더 이해하기 어려운 부분에 집중했습니다. 개념(이 개념을 이해하는 데 있어서 개인적인 어려움을 포함하여)은 보다 대중적인 방식으로 설명됩니다. 물론, 이 개념을 이제 막 배우기 시작했기 때문에 부적절한 실수를 저지르는 것은 불가피합니다. 보신다면 주저하지 마시고 저에게 깨우쳐주시길 바랍니다.

모든 것은 Iterator와 Generator로 시작됩니다. 이 글의 절반은 반복자 인터페이스(Iterator)와 생성기 클래스에 관한 것입니다. 이미 이해하고 있다면 바로 건너뛰셔도 됩니다.

반복 및 반복자

이 기사의 대부분의 개념을 이해하기 전에 반복 및 반복자를 알아야 합니다. 사실 반복이 무엇인지는 다들 알고 있지만 저는 잘 모릅니다(사실 이전에는 이 개념에 대한 체계적 이해가 없었습니다). 반복은 프로세스를 반복적으로 실행하는 것을 의미하며 각 실행을 반복이라고 합니다. 실제로 우리는 이런 일을 자주 합니다. 예를 들어:

<?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);
}
we can see through ).

PHP는 통합된 반복자 인터페이스를 제공합니다. 반복자에 대한 공식 PHP 문서에는 더 자세한 설명이 있으므로 읽어보는 것이 좋습니다.

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

간단한 반복자를 구현하는 예를 들어 보겠습니다. foreach 对数组遍历并迭代输出其内容。在这一环节中,我们需要关注的重点是数组。虽然我们迭代的过程是 foreach 语句中的代码块,但实际上数组 $mapping 在每一次迭代中发生了变化,意味着数组内部也存在着一次迭代。如果我们把数组看做一个对象,foreach 实际上在每一次迭代过程都会调用该对象的一个方法,让数组在自己内部进行一次变动(迭代),随后通过另一个方法取出当前数组对象的键和值。这样一个可通过外部遍历其内部数据的对象就是一个迭代器对象,其遵循的统一的访问接口就是迭代器接口(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;
  }
}

foreach 순회를 통해 이 반복자의 효과를 살펴보겠습니다.

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
#🎜 🎜#
지금까지 우리는 반복자의 구현을 살펴보았습니다. 어떤 사람들은 이 기능을 알고 나서 실제 프로젝트에 적용하게 되어 매우 기뻐하겠지만, 어떤 사람들은 이 기능이 무슨 용도인지 혼란스러워합니다. 반복자는 일반 개체를 통과할 수 있는 개체로 변환합니다(예: StudentsContact 개체). 이 개체는 addStudent 메서드를 통해 학생을 등록하고 getAllStudent를 통해 등록된 모든 학생을 가져오는 데 사용됩니다. 학생 연락처 정보 배열입니다. 과거에는 StudentsContact::getAllStudent()를 사용하여 배열을 가져온 다음 배열을 순회했지만 이제는 반복자를 사용하여 클래스가 이 인터페이스를 상속하는 한 객체를 직접 순회하여 학생 배열을 가져올 수 있습니다. 출력 데이터를 가져오기 전에 클래스에 추가할 수 있습니다.

물론 용도가 훨씬 많지만 여기서는 너무 얽매이지 않겠습니다. 이를 바탕으로 더 강력한 것이 생성기입니다.

Generator, Generator

간단히 인터페이스를 상속받아서 반복자를 구현할 수 있지만 여전히 매우 번거롭습니다. 클래스를 정의하고 이 인터페이스의 모든 메소드를 구현하는 것은 매우 지루한 작업입니다. 어떤 상황에서는 더 간단한 접근 방식이 필요합니다. 생성기는 Iterator 인터페이스를 구현하기 위해 클래스를 정의하는 것에 비해 성능 오버헤드와 복잡성을 크게 줄여 간단한 객체 반복을 구현하는 더 쉬운 방법을 제공합니다.

PHP 공식 문서에는 다음과 같이 나와 있습니다.

生成器允许你在 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(&#39;hello&#39;);
$printer->send(&#39;world&#39;);

上述例子输出内容为:

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());

输出内容如下:

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如何读取表格中内容

위 내용은 PHP에서 생성기와 코루틴은 어떻게 구현됩니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.