什么是协程_什么时候使用协程和线程

什么是协程_什么时候使用协程和线程先搞清楚,什么是协程。你可能已经听过『进程』和『线程』这两个概念。进程就是二进制可执行文件在计算机内存里的一个运行实例,就好比你的.exe文件是个类,进程就是new出来的那个实例。进程是计算机系

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

先搞清楚,什么是协程。

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

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

进程是计算机系统进行资源分配和调度的基本单位(调度单位这里别纠结线程进程的),每个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); }

这样,轻松调用子协程。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/167515.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • http 1.0 / 1.1 / 2.0的区别

    http 1.0 / 1.1 / 2.0的区别http1.0 / 1.1 / 2.0的区别

  • ubuntu安装增强功能失败_ubuntu参考的对象不支持

    ubuntu安装增强功能失败_ubuntu参考的对象不支持Ubuntu换源后,更新提示GPGerror缺少公钥W:GPGerror:http://mirrors.aliyun.comtrusty-securityInRelease:Thefollowingsignaturescouldn’tbeverifiedbecausethepublickeyisnotavailable:NO_PUBKEY40976EAF437D05B5NO_PUBKEY3B4FE6ACC0B21F32W:GPGerror:http:/

  • Linux查看网卡带宽[通俗易懂]

    Linux查看网卡带宽[通俗易懂]ifconfig查看网卡信息执行命令:ethtool网卡名称,例:ethtooleth1输出内容如下:Settingsforeth1:Supportedports:[FIBRE]Supportedlinkmodes:1000baseT/Full10000baseT/FullSupportedpauseframeuse:Symmetr..

    2022年10月19日
  • 苹果开发者账号多少钱?个人/公司/企业申请费用及怎么选【都有】

    苹果开发者账号多少钱?个人/公司/企业申请费用及怎么选【都有】最近在做APP开发,APP开发已经好了,现在需要将APP上架到苹果应用市场,顺便整理了一下苹果开发者账号的申请费用,大家有需要的可以看看。苹果开发者账号费用预览:苹果账号主要分为三种:个人版,公司

  • Java byte 转化为 String「建议收藏」

    Java byte 转化为 String「建议收藏」1、Java中byte转化为String,代码如下packagenice.com.mian;importjava.io.UnsupportedEncodingException;publicclassStringMain{ publicstaticvoidmain(String[]args)throwsException{ byte[]…

  • PyCharm与Python的安装教程2021.11

    PyCharm与Python的安装教程2021.11文章预览:一、进入官网下载PyCharm安装包二、等待下载完成后点击进行安装三、Python安装(只介绍最新版本)四、第一个PyCharm程序五、PyCharm安装第三方库方法一、进入官网下载PyCharm安装包自行进入PyCharm官网或点击https://www.jetbrains.com/pycharm/download/#section=windows下载的是社区版,免费并且足够使用。二、等待下载完成后点击进行安装1.点击next2.选择自己要安装的目录3.勾选一些设定(1

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号