资讯专栏INFORMATION COLUMN

PHP回顾之协程

Java3y / 780人阅读

摘要:本文先回顾生成器,然后过渡到协程编程。其作用主要体现在三个方面数据生成生产者,通过返回数据数据消费消费者,消费传来的数据实现协程。解决回调地狱的方式主要有两种和协程。重点应当关注控制权转让的时机,以及协程的运作方式。

转载请注明文章出处: https://tlanyan.me/php-review...
PHP回顾系列目录

PHP基础

web请求

cookie

web响应

session

数据库操作

加解密

Composer

创建自己的Composer包

发送邮件

IO

Socket编程

多进程编程

执行流程及相关概念

PHP自5.5起引入了生成器(Generator),基于其可实现协程编程。本文先回顾生成器,然后过渡到协程编程。

yield与生成器 生成器

生成器是一种数据类型,实现了iterator接口。不能通过new得到生成器实例,也没有获取生成器实例的静态方法。得到生成器实例的唯一办法是调用生成器函数(包含yield关键字的函数)。调用生成器函数直接返回一个生成器对象,生成器运行时函数内的代码才开始执行。

先上代码直观感受一下yield与生成器:

# generator1.php
function foo() {
    exit("exit script when generator runs.");
    yield;
}

$gen = foo();
var_dump($gen);
$gen->current();

echo "unreachable code!";

# 执行结果
object(Generator)#1 (0) {
}
exit script when generator runs.

foo函数包含yield关键字,变身为生成器函数。调用foo不会执行函数体中的任何代码,而是返回一个生成器实例。生成器运行后,foo函数内的代码执行,脚本结束。

如其名,生成器可以用来生成数据。只是其生成数据的方式与其他函数不一样:生成器通过yield返回数据,而非return; yield返回数据后,生成器函数不会销毁,只是暂停运行,未来可以从暂停处恢复运行;生成器运行一次,(只)返回一个数据,多次运行就返回多个数据;不调用生成器获取数据,生成器内的代码就躺着不动,所谓动次打次,说的就是生成器生成数据的样子。

生成器实现了迭代器接口,获取生成器数据可以用foreach循环或手工current/next/valid。如下代码演示数据生成和遍历:

# generator2.php
function foo() {
  # 返回键值对数据
  yield "key1" => "value1";
  $count = 0;
  while ($count < 5) {
    # 返回值,key自动生成
    yield $count;
    ++ $count;
  }
  # 不返回值,相当于返回null
  yield;
}

# 手动获取生成器数据
$gen = foo();
while ($gen->valid()) {
  fwrite(STDOUT, "key:{$gen->key()}, value:{$gen->current()}
");
  $gen->next();
}

# foreach 遍历数据
fwrite(STDOUT, "
data from foreach
");
foreach (foo() as $key => $value) {
    fwrite(STDOUT, "key:$key, value:$value
");
}
yield

yield关键字是生成器的核心,其让普通函数异化(进化)为生成器函数。yield有“让出”的意思,程序执行到yield语句会暂停执行,让出CPU并将控制权返回到调用者,下次执行时从中断点继续执行。控制权返回到调用者时,yield语句可以携带值返回给调用方。generator2.php脚本演示了yield返回值的三种形式:

yield $key => $value: 返回数据的key和value;

yield $value: 返回数据,key由系统分配;

yield: 返回null值,key由系统分配;

yield让函数可以随时暂停、继续执行,并返回数据给调用方。如果继续执行时需要外部数据,这个工作由生成器的send函数提供:出现在yield左边等号的变量会接收send传来的值。看一个常见的send函数使用样例:

function logger(string $filename) {
  $fd = fopen($filename, "w+");
  while($msg = yield) {
    fwrite($fd, date("Y-m-d H:i:s") . ":" . $msg . PHP_EOL);
  }
  fclose($fd);
}

$logger = logger("log.txt");
$logger->send("program starts!");
// do some thing
$logger->send("program ends!");

send让生成器之间和外部有双向数据通信的能力:yield返回数据;send提供继续运行的支撑数据。由于send让生成器继续执行,这个行为与迭代器的next接口类似,next相当于send(null)

其他

$string = yield $data;的表达式在PHP7前不合法,需要加括号:$string = (yield $data);

PHP5生成器函数不能return值,PHP7后可以return值,并通过生成器的getReturn获取返回的值。详情参考返回值的RFC:https://wiki.php.net/rfc/gene...;

PHP7新增了yield from语法,实现了生成器委托,详情请参考其RFC: https://wiki.php.net/rfc/gene...;

生成器是单向迭代器,开动后不能调用rewind

总结

相对于其他迭代器,生成器具有性能开销小、编码容易的特点。其作用主要体现在三个方面:

数据生成(生产者),通过yield返回数据;

数据消费(消费者),消费send传来的数据;

实现协程。

关于PHP中的生成器及基本用法,建议看看 2gua 大佬的博文:PHP之生成器,生动有趣且易懂。

协程编程

协程(coroutine)是随时可中断、恢复执行的子程序,yield关键字让函数拥有这种能力,所以可以用于协程编程。

进程、线程和协程

线程归属于进程,一个进程可有多个线程。进程是计算机分配资源的最小单位,线程是计算机调度执行的最小单位。进程和线程均由操作系统调度。

协程可以看成“用户态的线程”,需要用户程序实现调度。线程和进程由操作系统调度“抢占式”交替运行,协程主动让出CPU“协商式”交替运行。协程十分的轻量,协程切换不涉及线程切换,执行效率高,数目越多,越能体现协程的优势。

生成器和协程

生成器实现的协程属于无栈协程(stackless coroutine),即生成器函数只有函数帧,运行时附加到调用方的栈上执行。不同于功能强大的有栈协程(stackful coroutine),生成器暂停后无法控制程序走向,只能将控制权被动的归还调用者;生成器只能中断自身,不能中断整个协程。当然,生成器的好处便是效率高(暂停时只需保存程序计数器即可),实现简单。

协程编程

说到PHP中的协程编程,相信大部分人已经看过鸟哥转载(翻译)的这篇博文:在PHP中使用协程实现多任务调度。原文作者 nikic 是PHP的核心开发者,生成器功能的倡议者和实现人。想深入了解生成器及基于其的协程编程,nikic关于生成器的RFC和鸟哥网站上的文章必读。

nikic的文章,生成器部分好懂,看完后用yield写个xrange类似函数肯定毫无压力。为什么一进入协程,就有点懵逼呢?

先看看基于生成器的协程工作方式:协程协作式工作,即协程之间通过主动让出CPU达到多任务交替运行(即并发多任务,但不是并行);一个生成器可看成一个协程,执行到yield语句,让出CPU控制权回到调用方,调用方继续执行其他协程或其他代码。

再来看鸟哥博客理解的难点何在。协程非常轻量,一个系统中可以同时存在成千上万个协程(生成器)。而操作系统不会对协程调度,安排协程执行的工作就落到开发者身上。部分人看不懂鸟哥文章的协程部分,是因为里面说协程编程少(写协程主要就是写生成器函数),而是花笔墨实现了一个协程的调度器(scheduler或者kernel):模拟了操作系统,对所有协程进行公平调度。PHP开发一般的思维是:我写了这些代码,PHP引擎会调用我这些代码得到预期结果。而协程编程不仅要写干活的代码,还要写指导这些代码什么时候干活的代码。没有很好的把握作者的思维,理解起来自然会难一些。需要自行调度,这是生成器协程相对于原生协程(async/await形式)的一个缺点。

知道了协程是怎么回事,那么它能用来干什么?协程自行让出CPU来协作高效利用CPU,让出的时机当然应该是程序阻塞时。什么地方会让程序阻塞呢?用户态的代码鲜有阻塞,阻塞主要是系统调用。而系统调用的大头是IO,所以协程的主要应用场景在网络编程。为了让程序高性能、高并发,程序应该异步执行不能阻塞。既然异步执行,就需要通知和回调,写回调函数避免不了“回调地狱(callback hell)”的问题:代码可读性差,程序执行流程散落在层层回调函数中等。解决回调地狱的方式主要有两种:Promise和协程。协程能以同步的方式编写代码,在高性能网络编程(IO密集型)中是推荐的。

再回过头看PHP中的协程编程。PHP中基于生成器实现实现协程编程,优先推荐使用RecoilPHPAmp等协程框架。这些框架已经写好了调度器,在其上开发直接写生成器函数,内核会自动调度执行(想让一个函数以协程方式调度执行,在函数体内加上yield即可)。如果不想用yield方式进行协程编程,推荐swoole或其衍生框架,能做到类似golang的协程编程体验,又能享受PHP的开发效率。

如果想用原生态的做PHP协程编程,类似鸟哥博客中的调度器必不可少。调度器调度协程执行,协程中断后控制权又回到调度器中。所以调度器应该总是在主(事件)循环中,即CPU不在执行协程,就应当在执行调度器的代码。无协程运行时,调度器应当自我阻塞避免消耗CPU(鸟哥博客中使用了内置的select系统调用),等待事件到来再执行相应的协程。程序运行期间,除了调度器阻塞,协程在运行过程中不应该调用阻塞API。

总结

在协程编程中,yield的主要作用是将控制权转让,无需纠结于其返回值(基本上yield返回的值会在下次执行时直接send过来)。重点应当关注控制权转让的时机,以及协程的运作方式。

另外需要说明一点,协程和异步没有多大关系,还要看运行环境支撑。常规的PHP运行环境,即使用了promise/coroutine,也还是同步阻塞的。再牛逼的协程框架,sleep一下也不好使了。作为类比,即使JavaScript不使用promise/async这些技术,也是异步非阻塞的。

通过生成器和Promise,能实现类似于await的协程编程,相关代码在Github上很多,本文不再给出。

总结

本文先介绍了生成器的概念,重点是yield的用法及生成器的接口。协程部分则简要说了协程的原理,以及PHP协程编程中应当注意的事项。

感谢阅读,欢迎指正!

参考

http://php.net/manual/zh/lang...

http://php.net/manual/zh/clas...

https://wiki.php.net/rfc/gene...

https://wiki.php.net/rfc/gene...

https://zhuanlan.zhihu.com/p/...

http://www.laruence.com/2015/...

https://medium.com/async-php/...

https://blog.kghost.info/2011...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/30868.html

相关文章

  • Swoole4.x协程变量访问安全与协程连接池实现

    摘要:访问安全问题为什么说有访问安全问题呢传统地,在的的环境中,很少有遇到所谓变量安全访问问题。上下文管理器为了解决这个问题,我们引入协程上下文管理这样的概念,由此来实现每个协程环境内的数据隔离。 访问安全问题 为什么说有访问安全问题呢?传统地,在php的的环境中,很少有Phper遇到所谓变量安全访问问题。举个例子,代码大约如下: class db { protected stati...

    aisuhua 评论0 收藏0
  • 通读Python官方文档协程、Future与Task

    摘要:所以在第一遍阅读官方文档的时候,感觉完全是在梦游。通过或者等待另一个协程的结果或者异常,异常会被传播。接口返回的结果指示已结束,并赋值。取消与取消不同。调用将会向被包装的协程抛出。任务相关函数安排协程的执行。负责切换线程保存恢复。 Tasks and coroutines 翻译的python官方文档 这个问题的恶心之处在于,如果你要理解coroutine,你应该理解future和tas...

    mgckid 评论0 收藏0
  • Python迭代器、生成器、装饰器深入解读

    摘要:前言首先,明确可迭代对象迭代器和生成器这三个概念。迭代器对象传送门之迭代器实现原理首先明确它是一个带状态的对象。生成器是一种特殊的迭代器,它的返回值不是通过而是用。 前言 首先,明确可迭代对象、迭代器和生成器这三个概念。 可迭代对象(Iterable) 可迭代对象(Iterable Object),简单的来理解就是可以使用 for 来循环遍历的对象。比如常见的 list、set和di...

    codercao 评论0 收藏0
  • Swoole协程之旅-前篇

    摘要:协程完全有用户态程序控制,所以也被成为用户态的线程。目前支持协程的语言有很多,例如等。协程之旅前篇结束,下一篇文章我们将深入分析原生协程部分的实现。 写在最前   Swoole协程经历了几个里程碑,我们需要在前进的道路上不断总结与回顾自己的发展历程,正所谓温故而知新,本系列文章将分为协程之旅前、中、后三篇。 前篇主要介绍协程的概念和Swoole几个版本协程实现的主要方案技术; 中篇主...

    terasum 评论0 收藏0
  • 低调奢华有内涵 - 收藏集 - 掘金

    摘要:比较的是两个对象的内容是并发编程之协程异步后端掘金引言随着的盛行,相信大家今年多多少少都听到了异步编程这个概念。使用进行并发编程篇二掘金我们今天继续深入学习。 python 之机器学习库 scikit-learn - 后端 - 掘金一、 加载sklearn中的数据集datasets from sklearn import datasets iris = datasets.load_i...

    walterrwu 评论0 收藏0

发表评论

0条评论

Java3y

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<