前言

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

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

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

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

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

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

  1. 欢迎微博关注我 @码云 。2. 全互联网最全的PHP技能图谱:https://bruceit.com/skills

什么是协程

先搞清楚,什么是协程。

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

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

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

所谓的并发,只不过是看起来CPU好像同时能处理几件事情一样,对于单核CPU事实上在用很快的速度切换不同的进程。

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

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

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

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

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

所以就有了线程。

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

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

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

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

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

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

协程,有几个特点:

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

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

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)生成器正确使用

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

方法如下:

  1. foreach他
  2. send($value)
  3. 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->addTask(task1()); // 添加不同的闭包函数作为任务
$scheduler->addTask(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->addTask(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协程了吧?

建议不要使用PHP的Yield来实现协程,推荐使用swoole,2.0已经支持了协程,并附带了部分案例。

End...

 
 

PHP7下的协程实现 转的更多相关文章

  1. Unity怎样在Editor下运行协程(coroutine)

    在处理Unity5新的AssetBundle的时候,我有一个需求,须要在Editor下(比方一个menuitem的处理函数中,游戏没有执行.也没有MonoBehaviour)载入AssetBundle ...

  2. 协程的作用 Python

    1.协程的含义和实现 协程是单进程单线程的超越函数的调度机制,它通过一定的调度手段进行调度. (Python使用generator机制,greenlet使用汇编控制对程序指向来实现). 2.协程有什么 ...

  3. 写个百万级别full-stack小型协程库——原理介绍

    其实说什么百万千万级别都是虚的,下面给出实现原理和测试结果,原理很简单,我就不上图了: 原理:为了简单明了,只支持单线程,每个协程共享一个4K的空间(你可以用堆,用匿名内存映射或者直接开个数组也都是可 ...

  4. 基于协程的Python网络库gevent

    import gevent def test1(): print 12 gevent.sleep(0) print 34 def test2(): print 56 gevent.sleep(0) p ...

  5. Unity C#笔记 协程

    什么是协程 协同程序,在主程序运行的同时,开启另外一段逻辑处理,来协同当前程序的执行. 可能看了这段文字介绍还是有点模糊,其实可以用多线程来比较. 多线程 多线程,顾名思义,多条同时执行的线程. 最初 ...

  6. Python3 与 C# 并发编程之~ 协程篇

      3.协程篇¶ 去年微信公众号就陆陆续续发布了,我一直以为博客也汇总同步了,这几天有朋友说一直没找到,遂发现,的确是漏了,所以补上一篇 在线预览:https://github.lesschina.c ...

  7. python中的协程

    目录 协程是啥 协程和线程差异 简单实现协程 greenlet 安装方式 gevent 安装 1. gevent的使用 2. gevent切换执行 3. 给程序打补丁 进程.线程.协程对比 请仔细理解 ...

  8. Python协程(真才实学,想学的进来)

    真正有知识的人的成长过程,就像麦穗的成长过程:麦穗空的时候,麦子长得很快,麦穗骄傲地高高昂起,但是,麦穗成熟饱满时,它们开始谦虚,垂下麦芒. --蒙田<蒙田随笔全集> *** 上篇论述了关 ...

  9. 进程池线程池 协程 gvent 单线程实现并发套接字

    1.基于多线程实现套接字服务端支持并发 服务端 from socket import * from threading import Thread def comunicate(conn): whil ...

随机推荐

  1. 查看windows和linux下端口是否被占用

    1.windows cmd输入netstat -ano |findstr "端口号" 查看到1202端口被使用的进程PID是10692 输入tasklist |findstr 10 ...

  2. JVM运行时数据区--程序计数器

    JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息.CPU只有把数据装载到寄存器才能够运行.JVM中 ...

  3. CSDN博客图片水印自定义及去除方法

    小图上传说明 尺寸较小的图片上传后默认不加水印,而且默认水平居中显示.你可以更改markdown格式后面的#pic_center,这是设置图片水平对齐的方式.#pic_left就是左对齐,#pic_r ...

  4. java 多线程-3

    十.同步机制解决Thread继承安全问题 创建三个窗口买票,共100张票.用继承来实现 方式一:同步代码块 public class RunMainExtends { public static vo ...

  5. PooledByteBuf内存池-------这个我现在不太懂

    转载自:http://blog.csdn.net/youaremoon/article/details/47910971              http://blog.csdn.net/youar ...

  6. 4.案例 - NIO实现TCP通信

    服务端: package cn.tedu.nio.channel; import java.net.InetSocketAddress; import java.nio.ByteBuffer; imp ...

  7. Java多线程--CAS

    在Java多线程并发的情况下同时对一个变量进行操作会出现线程安全的问题,假如我们现在使用20个线程对一个变量不停累加1,代码如下: 1 public class ThreadDemo implemen ...

  8. Java高级开发必会的50个性能优化的细节(珍藏版)

      在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身.养成良好的编码习惯非常重要,能够显著地提升程序性能. ● 1. 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短 ...

  9. 白话ansible-runner--1.环境搭建

    最近在Windows10上的项目需要使用到ansible API调用,参考 本末大神 推荐ansible API用官网封装的ansible-runner开发比较友好,ansible-runner是an ...

  10. python条件控制语句要注意什么?本文详解

    1.条件判断语句(if语句) 执⾏的流程:if语句在执⾏时,会先对条件表达式进⾏求值判断, 如果为True,则执⾏if后的语句 如果为False,则不执⾏ 语法: if 条件表达式 : 代码块 代码块 ...