PHP7下协程的实现方法详解

前言

相信大家都听说过『协程』这个概念吧。

但是有些同学对这个概念似懂非懂,不知道怎么实现,怎么用,用在哪,甚至有些人认为yield就是协程!

我始终相信,如果你无法准确地表达出一个知识点的话,我可以认为你就是不懂。

如果你之前了解过利用PHP实现协程的话,你肯定看过鸟哥的那篇文章:在PHP中使用协程实现多任务调度| 风雪之隅

鸟哥这篇文章是从国外的作者翻译来的,翻译的简洁明了,也给出了具体的例子了。

我写这篇文章的目的,是想对鸟哥文章做更加充足的补充,毕竟有部分同学的基础还是不够好,看得也是云头雾里的。

什么是协程

先搞清楚,什么是协程。

你可能已经听过『进程』和『线程』这两个概念。

进程就是二进制可执行文件在计算机内存里的一个运行实例,就好比你的.exe文件是个类,进程就是new出来的那个实例。

进程是计算机系统进行资源分配和调度的基本单位(调度单位这里别纠结线程进程的),每个CPU下同一时刻只能处理一个进程。

所谓的并行,只不过是看起来并行,CPU事实上在用很快的速度切换不同的进程。

进程的切换需要进行系统调用,CPU要保存当前进程的各个信息,同时还会使CPUCache被废掉。

所以进程切换不到费不得已就不做。

那么怎么实现『进程切换不到费不得已就不做』呢?

首先进程被切换的条件是:进程执行完毕、分配给进程的CPU时间片结束,系统发生中断需要处理,或者进程等待必要的资源(进程阻塞)等。你想下,前面几种情况自然没有什么话可说,但是如果是在阻塞等待,是不是就浪费了。

其实阻塞的话我们的程序还有其他可执行的地方可以执行,不一定要傻傻的等!

所以就有了线程。

线程简单理解就是一个『微进程』,专门跑一个函数(逻辑流)。

所以我们就可以在编写程序的过程中将可以同时运行的函数用线程来体现了。

线程有两种类型,一种是由内核来管理和调度。

我们说,只要涉及需要内核参与管理调度的,代价都是很大的。这种线程其实也就解决了当一个进程中,某个正在执行的线程遇到阻塞,我们可以调度另外一个可运行的线程来跑,但是还是在同一个进程里,所以没有了进程切换。

还有另外一种线程,他的调度是由程序员自己写程序来管理的,对内核来说不可见。这种线程叫做『用户空间线程』。

协程可以理解就是一种用户空间线程。

协程,有几个特点:

  • 协同,因为是由程序员自己写的调度策略,其通过协作而不是抢占来进行切换
  • 在用户态完成创建,切换和销毁
  • ⚠️ 从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制
  • 迭代器经常用来实现协程

说到这里,你应该明白协程的基本概念了吧?

PHP实现协程

一步一步来,从解释概念说起!

可迭代对象

PHP5提供了一种定义对象的方法使其可以通过单元列表来遍历,例如用foreach语句。

你如果要实现一个可迭代对象,你就要实现Iterator接口:

<?php
class MyIterator implements Iterator
{
 private $var = array();
 public function __construct($array)
 {
  if (is_array($array)) {
   $this->var = $array;
  }
 }
 public function rewind() {
  echo "rewinding\n";
  reset($this->var);
 }
 public function current() {
  $var = current($this->var);
  echo "current: $var\n";
  return $var;
 }
 public function key() {
  $var = key($this->var);
  echo "key: $var\n";
  return $var;
 }
 public function next() {
  $var = next($this->var);
  echo "next: $var\n";
  return $var;
 }
 public function valid() {
  $var = $this->current() !== false;
  echo "valid: {$var}\n";
  return $var;
 }
}
$values = array(1,2,3);
$it = new MyIterator($values);
foreach ($it as $a => $b) {
 print "$a: $b\n";
}

生成器

可以说之前为了拥有一个能够被foreach遍历的对象,你不得不去实现一堆的方法,yield关键字就是为了简化这个过程。

生成器提供了一种更容易的方法来实现简单的对象迭代,相比较定义类实现Iterator接口的方式,性能开销和复杂性大大降低。

<?php
function xrange($start, $end, $step = 1) {
 for ($i = $start; $i <= $end; $i += $step) {
  yield $i;
 }
}
foreach (xrange(1, 1000000) as $num) {
 echo $num, "\n";
}

记住,一个函数中如果用了yield,他就是一个生成器,直接调用他是没有用的,不能等同于一个函数那样去执行!

所以,yield就是yield,下次谁再说yield是协程,我肯定把你xxxx。

PHP协程

前面介绍协程的时候说了,协程需要程序员自己去编写调度机制,下面我们来看这个机制怎么写。

0)生成器正确使用

既然生成器不能像函数一样直接调用,那么怎么才能调用呢?

方法如下:

  • foreach他
  • send($value)
  • current / next...

1)Task实现

Task就是一个任务的抽象,刚刚我们说了协程就是用户空间协程,线程可以理解就是跑一个函数。

所以Task的构造函数中就是接收一个闭包函数,我们命名为coroutine。

/**
 * Task任务类
 */
class Task
{
 protected $taskId;
 protected $coroutine;
 protected $beforeFirstYield = true;
 protected $sendValue;

 /**
  * Task constructor.
  * @param $taskId
  * @param Generator $coroutine
  */
 public function __construct($taskId, Generator $coroutine)
 {
  $this->taskId = $taskId;
  $this->coroutine = $coroutine;
 }
 /**
  * 获取当前的Task的ID
  *
  * @return mixed
  */
 public function getTaskId()
 {
  return $this->taskId;
 }
 /**
  * 判断Task执行完毕了没有
  *
  * @return bool
  */
 public function isFinished()
 {
  return !$this->coroutine->valid();
 }
 /**
  * 设置下次要传给协程的值,比如 $id = (yield $xxxx),这个值就给了$id了
  *
  * @param $value
  */
 public function setSendValue($value)
 {
  $this->sendValue = $value;
 }
 /**
  * 运行任务
  *
  * @return mixed
  */
 public function run()
 {
  // 这里要注意,生成器的开始会reset,所以第一个值要用current获取
  if ($this->beforeFirstYield) {
   $this->beforeFirstYield = false;
   return $this->coroutine->current();
  } else {
   // 我们说过了,用send去调用一个生成器
   $retval = $this->coroutine->send($this->sendValue);
   $this->sendValue = null;
   return $retval;
  }
 }
}

2)Scheduler实现

接下来就是Scheduler这个重点核心部分,他扮演着调度员的角色。

/**
 * Class Scheduler
 */
Class Scheduler
{
 /**
  * @var SplQueue
  */
 protected $taskQueue;
 /**
  * @var int
  */
 protected $tid = 0;

 /**
  * Scheduler constructor.
  */
 public function __construct()
 {
  /* 原理就是维护了一个队列,
   * 前面说过,从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制
   * */
  $this->taskQueue = new SplQueue();
 }
 /**
  * 增加一个任务
  *
  * @param Generator $task
  * @return int
  */
 public function addTask(Generator $task)
 {
  $tid = $this->tid;
  $task = new Task($tid, $task);
  $this->taskQueue->enqueue($task);
  $this->tid++;
  return $tid;
 }
 /**
  * 把任务进入队列
  *
  * @param Task $task
  */
 public function schedule(Task $task)
 {
  $this->taskQueue->enqueue($task);
 }
 /**
  * 运行调度器
  */
 public function run()
 {
  while (!$this->taskQueue->isEmpty()) {
   // 任务出队
   $task = $this->taskQueue->dequeue();
   $res = $task->run(); // 运行任务直到 yield

   if (!$task->isFinished()) {
    $this->schedule($task); // 任务如果还没完全执行完毕,入队等下次执行
   }
  }
 }
}

这样我们基本就实现了一个协程调度器。

你可以使用下面的代码来测试:

<?php
function task1() {
 for ($i = 1; $i <= 10; ++$i) {
  echo "This is task 1 iteration $i.\n";
  yield; // 主动让出CPU的执行权
 }
}
function task2() {
 for ($i = 1; $i <= 5; ++$i) {
  echo "This is task 2 iteration $i.\n";
  yield; // 主动让出CPU的执行权
 }
}
$scheduler = new Scheduler; // 实例化一个调度器
$scheduler->newTask(task1()); // 添加不同的闭包函数作为任务
$scheduler->newTask(task2());
$scheduler->run();

关键说下在哪里能用得到PHP协程。

function task1() {
  /* 这里有一个远程任务,需要耗时10s,可能是一个远程机器抓取分析远程网址的任务,我们只要提交最后去远程机器拿结果就行了 */
  remote_task_commit();
  // 这时候请求发出后,我们不要在这里等,主动让出CPU的执行权给task2运行,他不依赖这个结果
  yield;
  yield (remote_task_receive());
  ...
}
function task2() {
 for ($i = 1; $i <= 5; ++$i) {
  echo "This is task 2 iteration $i.\n";
  yield; // 主动让出CPU的执行权
 }
}

这样就提高了程序的执行效率。

关于『系统调用』的实现,鸟哥已经讲得很明白,我这里不再说明。

3)协程堆栈

鸟哥文中还有一个协程堆栈的例子。

我们上面说过了,如果在函数中使用了yield,就不能当做函数使用。

所以你在一个协程函数中嵌套另外一个协程函数:

<?php
function echoTimes($msg, $max) {
 for ($i = 1; $i <= $max; ++$i) {
  echo "$msg iteration $i\n";
  yield;
 }
}
function task() {
 echoTimes('foo', 10); // print foo ten times
 echo "---\n";
 echoTimes('bar', 5); // print bar five times
 yield; // force it to be a coroutine
}
$scheduler = new Scheduler;
$scheduler->newTask(task());
$scheduler->run();

这里的echoTimes是执行不了的!所以就需要协程堆栈。

不过没关系,我们改一改我们刚刚的代码。

把Task中的初始化方法改下,因为我们在运行一个Task的时候,我们要分析出他包含了哪些子协程,然后将子协程用一个堆栈保存。(C语言学的好的同学自然能理解这里,不理解的同学我建议去了解下进程的内存模型是怎么处理函数调用)

 /**
  * Task constructor.
  * @param $taskId
  * @param Generator $coroutine
  */
 public function __construct($taskId, Generator $coroutine)
 {
  $this->taskId = $taskId;
  // $this->coroutine = $coroutine;
  // 换成这个,实际Task->run的就是stackedCoroutine这个函数,不是$coroutine保存的闭包函数了
  $this->coroutine = stackedCoroutine($coroutine);
 }

当Task->run()的时候,一个循环来分析:

/**
 * @param Generator $gen
 */
function stackedCoroutine(Generator $gen)
{
 $stack = new SplStack;
 // 不断遍历这个传进来的生成器
 for (; ;) {
  // $gen可以理解为指向当前运行的协程闭包函数(生成器)
  $value = $gen->current(); // 获取中断点,也就是yield出来的值
  if ($value instanceof Generator) {
   // 如果是也是一个生成器,这就是子协程了,把当前运行的协程入栈保存
   $stack->push($gen);
   $gen = $value; // 把子协程函数给gen,继续执行,注意接下来就是执行子协程的流程了
   continue;
  }
  // 我们对子协程返回的结果做了封装,下面讲
  $isReturnValue = $value instanceof CoroutineReturnValue; // 子协程返回`$value`需要主协程帮忙处理
  if (!$gen->valid() || $isReturnValue) {
   if ($stack->isEmpty()) {
    return;
   }
   // 如果是gen已经执行完毕,或者遇到子协程需要返回值给主协程去处理
   $gen = $stack->pop(); //出栈,得到之前入栈保存的主协程
   $gen->send($isReturnValue ? $value->getValue() : NULL); // 调用主协程处理子协程的输出值
   continue;
  }
  $gen->send(yield $gen->key() => $value); // 继续执行子协程
 }
}

然后我们增加echoTime的结束标示:

class CoroutineReturnValue {
 protected $value;

 public function __construct($value) {
  $this->value = $value;
 }
 // 获取能把子协程的输出值给主协程,作为主协程的send参数
 public function getValue() {
  return $this->value;
 }
}
function retval($value) {
 return new CoroutineReturnValue($value);
}

然后修改echoTimes:

function echoTimes($msg, $max) {
 for ($i = 1; $i <= $max; ++$i) {
  echo "$msg iteration $i\n";
  yield;
 }
 yield retval(""); // 增加这个作为结束标示
}

Task变为:

function task1()
{
 yield echoTimes('bar', 5);
}

这样就实现了一个协程堆栈,现在你可以举一反三了。

4)PHP7中yield from关键字

PHP7中增加了yield from,所以我们不需要自己实现携程堆栈,真实太好了。

把Task的构造函数改回去:

 public function __construct($taskId, Generator $coroutine)
 {
  $this->taskId = $taskId;
  $this->coroutine = $coroutine;
  // $this->coroutine = stackedCoroutine($coroutine); //不需要自己实现了,改回之前的
 }

echoTimes函数:

function echoTimes($msg, $max) {
 for ($i = 1; $i <= $max; ++$i) {
  echo "$msg iteration $i\n";
  yield;
 }
}

task1生成器:

function task1()
{
 yield from echoTimes('bar', 5);
}

这样,轻松调用子协程。

总结

这下应该明白怎么实现PHP协程了吧?

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 关于PHP中协程和阻塞的一些理解与思考

    前言 本文主要给大家介绍了关于PHP中协程和阻塞的理解与思考,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: 进程.线程.协程 关于进程.线程.协程,有非常详细和丰富的博客或者学习资源,我不在此做赘述,我大致在此介绍一下这几个东西. 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度. 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的). 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度. PHP中的协程实现

  • PHP7下协程的实现方法详解

    前言 相信大家都听说过『协程』这个概念吧. 但是有些同学对这个概念似懂非懂,不知道怎么实现,怎么用,用在哪,甚至有些人认为yield就是协程! 我始终相信,如果你无法准确地表达出一个知识点的话,我可以认为你就是不懂. 如果你之前了解过利用PHP实现协程的话,你肯定看过鸟哥的那篇文章:在PHP中使用协程实现多任务调度| 风雪之隅 鸟哥这篇文章是从国外的作者翻译来的,翻译的简洁明了,也给出了具体的例子了. 我写这篇文章的目的,是想对鸟哥文章做更加充足的补充,毕竟有部分同学的基础还是不够好,看得也是云

  • Golang控制协程执行顺序方法详解

    目录 循环控制 通道控制 互斥锁 async.Mutex 在 Go 里面的协程执行实际上默认是没有严格的先后顺序的.由于 Go 语言 GPM 模型的设计理念,真正执行实际工作的实际上是 GPM 中的 M(machine) 执行器,而我们的协程任务 G(goroutine) 协程需要被 P(produce) 关联到某个 M 上才能被执行.而每一个 P 都有一个私有队列,除此之外所有的 P 还共用一个公共队列.因此当我们创建了一个协程之后,并不是立即执行,而是进入队列等待被分配,且不同队列之间没有顺

  • PHP生成器(generator)和协程的实现方法详解

    本文实例讲述了PHP生成器(generator)和协程的实现方法.分享给大家供大家参考,具体如下: 先说一些废话 PHP 5.5 以来,新的诸多特性又一次令 PHP 焕发新的光彩,虽然在本文写的时候已是 PHP 7 alpha 2 发布后的一段时间,但此时国内依旧是 php 5.3 的天下.不过我认为新的特性迟早会因为旧的版本的逐渐消失而变得越发重要,尤其是 PHP 7 的正式版出来后,因此本文的目的就是为了在这之前,帮助一些 PHPer 了解一些他们从没有了解的东西.所以打算将以本篇作为博客中

  • Python并发编程协程(Coroutine)之Gevent详解

    Gevent官网文档地址:http://www.gevent.org/contents.html 基本概念 我们通常所说的协程Coroutine其实是corporateroutine的缩写,直接翻译为协同的例程,一般我们都简称为协程. 在linux系统中,线程就是轻量级的进程,而我们通常也把协程称为轻量级的线程即微线程. 进程和协程 下面对比一下进程和协程的相同点和不同点: 相同点: 我们都可以把他们看做是一种执行流,执行流可以挂起,并且后面可以在你挂起的地方恢复执行,这实际上都可以看做是con

  • Kotlin协程Dispatchers原理示例详解

    目录 前置知识 demo startCoroutineCancellable intercepted()函数 DefaultScheduler中找dispatch函数 Runnable传入 Worker线程执行逻辑 小结 前置知识 Kotlin协程不是什么空中阁楼,Kotlin源代码会被编译成class字节码文件,最终会运行到虚拟机中.所以从本质上讲,Kotlin和Java是类似的,都是可以编译产生class的语言,但最终还是会受到虚拟机的限制,它们的代码最终会在虚拟机上的某个线程上被执行. 之

  • inux下gettimeofday函数windows替换方法(详解)

    实例如下: #include <time.h> #ifdef WIN32 # include <windows.h> #else # include <sys/time.h> #endif #ifdef WIN32 int gettimeofday(struct timeval *tp, void *tzp) { time_t clock; struct tm tm; SYSTEMTIME wtm; GetLocalTime(&wtm); tm.tm_year

  • Linux环境下Oracle安装参数设置方法详解

    前面讲了虚拟机的设置和OracleLinux的安装,接下来我们来说下Oracle安装前的准备工作. 1.系统信息查看 系统信息查看 首先服务器ip:192.168.8.120 服务器系统:Oracle Linux Server release 6.5 服务器主机名:oracle-learn 查看磁盘空间情况: [root@oracle-learn ~]# df -h Filesystem Size Used Avail Use% Mounted on /dev/sda1 32G 4.8G 26G

  • 在linux下升级软件包版本等方法详解

    Linux环境下,要想查看某个软件(package)是否安装. rpm包方式安装的,使用 rpm -qa | grep "软件或者包的名字". yum方式安装的, yum list installed | grep "软件或者包的名字". 升级软件包版本. 我们经常会遇到依赖其他版本包的情况(一般是更新的版本),这时候我们需要升级包. 1.卸载后安装新的包. 首先要根据文首提到的查看软件包是否安装的方式查看你是否安装了这个软件,若没有安装,找到路径后使用wget命令

  • 对angularjs框架下controller间的传值方法详解

    AngularJS中的controller是个函数,用来向视图的作用域($scope)添加额外的功能,我们用它来给作用域对象设置初始状态,并添加自定义行为. 当我们在创建新的控制器时,angularJS会帮我们生成并传递一个新的$scope对象给这个controller,在angularJS应用的中的任何一个部分,都有父级作用域的存在,顶级就是ng-app所在的层级,它的父级作用域就是$rootScope. 每个$scope的$root指向$rootScope, $cope.$parent指向父

  • IDEA下使用MyBatisCodeHelper插件的方法详解

    一:IDEA下使用MyBatisCodeHelper插件.(IDEA的版本 2018 2.5亲测可用) 1.1  IDEA安装MyBatisCodeHelper插件. MyBatisCodeHelper插件现在要付费了,有条件可以买激活码激活的,支持正版,这里演示安装硬盘下载的插件的方式来介绍安装. 下载链接: 链接: https://pan.baidu.com/s/1I-XlrPPVAuzsu9_XY0eEUQ 提取码: bqvn ,内含多个版本的.下载解压查看. Mybatis-Plugin

随机推荐