前言相信大家都听说过“协程”这个概念。但是有的同学好像对这个概念很了解,不知道怎么实现,怎么用,用在什么地方。甚至有人认为yield是协程!我一直认为,如果你不能准确地表达一个知识点,我可以认为你就是没听懂。如果你之前学习过使用PHP实现协程,那你一定看过鸟哥的文章:在PHP中使用协程实现多任务调度|风雪角落里的鸟哥这篇文章是国外作者翻译的不错,翻译简洁明了,还给出了具体的例子。我写这篇文章的目的是为了对鸟哥的文章做一个更充分的补充。毕竟有些同学的基础还是不够好,也是云里雾里。欢迎关注我的微博@码云。2.网上最全的PHP技能地图:https://bruceit.com/skills什么是协程,先搞清楚什么是协程。你可能听说过“进程”和“线程”这两个概念。进程是一个二进制可执行文件在计算机内存中运行的实例,就像你的.exe文件是一个类,进程就是new出来的实例。进程是计算机系统中进行资源分配和调度的基本单位(这里的调度单位与线程进程无关),每个CPU一次只能处理一个进程。所谓并发,就是CPU好像可以同时处理几件事情。对于单核CPU来说,其实是在以非常快的速度切换不同的进程。进程切换需要系统调用,CPU需要保存当前进程的各种信息,同时CPUCache会被废除。因此,只有在绝对必要时才会进行进程切换。那么如何实现“不到万不得已,不做进程切换”呢?首先,一个进程被切换的条件是:进程执行完毕,分配给进程的CPU时间片结束,需要处理系统中断,或者进程正在等待必要的资源(processblocking)等,仔细想想,前几种情况自然无话可说,但如果是阻塞等待,岂不是浪费?其实如果被阻塞了,我们的程序还有其他可执行的地方可以执行,我们不用傻等!所以有线程。简单理解线程就是一个“微进程”,运行一个函数(逻辑流程)。所以我们在编写程序的过程中可以使用线程来体现可以同时运行的功能。线程有两种,一种是由内核管理和调度的。我们说只要涉及到内核参与管理和调度,成本就很高。这种线程其实解决了当一个正在执行的线程在一个进程中遇到阻塞的时候,我们可以调度另一个可运行的线程运行,但是它还是在同一个进程中,所以不存在进程切换。还有一种线程,它的调度是由程序员自己写程序管理的,对内核是不可见的。这种线程称为“用户空间线程”。协程可以理解为用户空间线程。协程有几个特点:协同,因为是程序员自己写的调度策略,通过协同而不是抢占来切换,在用户态完成创建、切换和销毁??从编程的角度来说,协程的思想在本质上是控制流的主动yield和resume机制。生成器通常用于实现协程。说到这里,你应该明白协程的基本概念了吧?PHP一步步实现协程,从讲解概念开始!可迭代对象PHP5提供了一种方法来定义可以遍历单元格列表的对象,例如使用foreach语句。如果要实现可迭代对象,需要实现Iterator接口:var=$array;}}publicfunctionrewind(){echo"倒带\n";重置($this->var);}publicfunctioncurrent(){$var=current($this->var);echo"current:$var\n";返回$var;}publicfunctionkey(){$var=key($this->var);echo"key:$var\n";返回$var;next(){$var=next($this->var);echo"下一个:$var\n";返回$var;}publicfunctionvalid(){$var=$this->current()!==false;echo"有效:{$var}\n";返回$var;}}$values=array(1,2,3);$it=newMyIterator($values);foreach($itas$a=>$b){print"$a:$b\n";}generator可以说,要想有一个foreach可以遍历的对象,就得实现一堆方法。yield关键字是为了简化这个过程,生成器提供了一种更简单的方法来实现简单的对象迭代,与定义类来实现Iterator接口的方式相比,性能开销和复杂度大大降低。taskId=$taskId;$this->coroutine=$coroutine;}/***获取当前Task的ID**@returnmixed*/publicfunctiongetTaskId(){return$this->taskId;}/***判断Task是否已经执行。**@returnbool*/publicfunctionisFinished(){return!$this->coroutine->valid();}/***设置下一次要传给协程的值,比如$id=(yield$xxxx),这个值会传给$id**@param$value*/publicfunctionsetSendValue($value){$this->sendValue=$value;}/***运行任务**@returnmixed*/publicfunctionrun(){//这里注意生成器一开始会重置,所以第一个值使用current获取if($this->beforeFirstYield){$this->beforeFirstYield=false;返回$this->coroutine->current();}else{//正如我们所说,使用send调用生成器$retval=$this->coroutine->send($this->sendValue);$this->sendValue=null;返回$retval;}}}2)Scheduler实现接下来是Scheduler的关键核心部分,起到调度器的作用/***类调度器*/类调度器{/***@varSplQueue*/protected$taskQueue;/***@varint*/protected$tid=0;/***调度器构造函数。*/publicfunction__construct(){/*原理是维护一个队列,*前面说过,从编程的角度来说,协程的思想本质上就是控制流主动yield和resume的机制**/$this->taskQueue=newSplQueue();}/***添加任务**@paramGenerator$task*@returnint*/publicfunctionaddTask(Generator$task){$tid=$this->tid;$task=newTask($tid,$task);$this->taskQueue->enqueue($task);$this->tid++;返回$tid;}/***入队任务**@paramTask$task*/publicfunctionschedule(Task$task){$this->taskQueue->enqueue($task);}/***运行调度器*/publicfunctionrun(){while(!$this->taskQueue->isEmpty()){//任务出队$task=$this->taskQueue->dequeue();$res=$task->run();//运行任务直到yieldif(!$task->isFinished()){$this->schedule($task);//如果任务还没有执行完,就加入队列,等待下一次执行}}}}这样,我们就基本实现了一个协程调度器。您可以使用以下代码进行测试:addTask(task1());//添加不同的闭包函数作为任务$scheduler->addTask(task2());$scheduler->run();关键是说PHP协程可以用在什么地方。functiontask1(){/*这里有一个远程任务,耗时10s。它可能是一台远程机器抓取和分析远程网站。我们只需要提交到远程机器就可以得到结果*/remote_task_commit();//此时请求发出后,我们不要在这里等待,主动让出CPU的执行权给task2,不依赖于结果yield;产量(remote_task_receive());...}functiontask2(){for($i=1;$i<=5;++$i){echo"这是任务2迭代$i.\n";屈服;//主动放弃CPU的执行权}}这样可以提高程序的执行效率。关于“系统调用”的实现,鸟哥已经解释的很清楚了,这里就不多说了。3)协程栈鸟文中也有一个协程栈的例子。上面我们说了,如果yield用在函数中,就不能当作函数来使用。所以你在另一个协程函数中嵌套了一个协程函数:addTask(任务());$调度程序->运行();这里的echoTimes是不能执行的!所以你需要一个协程栈。不过没关系,我们把刚才写的代码改一下。改变Task中的初始化方式,因为我们在运行一个Task的时候,需要分析它包含了哪些子协程,然后把子协程用一个栈保存起来。(学好C语言的同学自然能看懂,不懂的同学建议先了解一下进程的内存模型是如何处理函数调用的)/***任务构造函数。*@param$taskId*@paramGenerator$coroutine*/publicfunction__construct($taskId,Generator$coroutine){$this->taskId=$taskId;//$this->coroutine=$coroutine;//替换这个,实际的Task->run是stackedCoroutine函数,不是$coroutine保存的闭包函数$this->coroutine=stackedCoroutine($coroutine);}当Task->run()时,循环分析:/***@paramGenerator$gen*/functionstackedCoroutine(Generator$gen){$stack=newSplStack;//不断遍历传入的generatorfor(;;){//$gen可以理解为指向当前运行的协程闭包函数(generator)$value=$gen->current();//获取中断点,即从yield中取值if($valueinstanceofGenerator){//如果也是生成器,则此为子协程,将当前运行的协程压入栈中以保存$stack->push($gen);$gen=$value;//将子协程函数交给gen继续执行。注意下一步是执行子协程流程continue;}//我们封装了子协程返回的结果,$isReturnValue=$valueinstanceof协程返回值;//子协程返回`$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的结束标志:classCoroutineReturnValue{protected$value;公共函数__construct($value){$this->value=$value;}//获取函数将子协程的输出值作为主协程的发送参数传递给主协程publicfunctiongetValue(){return$this->value;}}functionretval($value){returnnewCoroutineReturnValue($value);}然后修改echoTimes:functionechoTimes($msg,$max){for($i=1;$i<=$max;++$i){echo"$msg迭代$i\n";屈服;}收益率等值(“”);//添加这个作为结束标记}任务变成:functiontask1(){yieldechoTimes('bar',5);}这样就实现了一个协程栈,现在可以举一反三了4)yieldfrom关键字inPHP7已经在PHP7中加入了yieldfrom,这样我们就不用自己去实现携程栈了,太好了。将Task构造函数改回:publicfunction__construct($taskId,Generator$coroutine){$this->taskId=$taskId;$this->coroutine=$coroutine;//$this->coroutine=stackedCoroutine($coroutine);//不用自己实现,改回之前的}echoTimesfunction:functionechoTimes($msg,$max){for($i=1;$i<=$max;++$i){echo"$msg迭代$i\n";屈服;}}task1generator:functiontask1(){yieldfromechoTimes('bar',5);}这样调用子协程就很方便了。综上所述,你应该明白PHP协程是如何实现的吧?不建议使用PHP的Yield来实现协程。推荐使用swoole。2.0已经支持协程,附上部分案例。结尾...
