关于PHP协程与阻塞的思考

进程、线程、协程

关于进程、线程、协程,有非常详细和丰富的博客或者学习资源,我不在此做赘述,我大致在此介绍一下这几个东西。

  1. 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
  2. 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。
  3. 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。

PHP中的协程实现基础 yield

yield的根本实现是生成器类,而迭代器类是迭代器接口的实现:

Generator implements Iterator {
    public mixed current ( void ) // 返回当前产生的值
    public mixed key ( void ) // 返回当前产生的键
    public void next ( void ) // 生成器继续执行
    public void rewind ( void ) // 重置迭代器,如果迭代已经开始了,这里会抛出一个异常。
                                             // renwind的执行将会导致第一个yield被执行, 并且忽略了他的返回值.
    public mixed send ( mixed $value ) // 向生成器中传入一个值,并且当做 yield 表达式的结果,然后继续执行生成器。如果当这个方法被调用时,生成器   
                                            // 不在 yield 表达式,那么在传入值之前,它会先运行到第一个 yield 表达式。
    public void throw ( Exception $exception ) // 向生成器中抛入一个异常
    public bool valid ( void ) // 检查迭代器是否被关闭
    public void __wakeup ( void ) // 序列化回调,抛出一个异常以表示生成器不能被序列化。
}

以上解析可以参考PHP官方文档。

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

以及鸟哥翻译的这篇详细文档:

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

我就以他实现的协程多任务调度为基础做一下例子说明并说一下关于我在阻塞方面所做的一些思考。

自定义简单定时执行任务示例:

(此例子必须依赖于以上鸟哥实现的协程调度代码)



class timer {
    private $start = 0; // 定时开始时间
    private $timer; // 间隔的时间差,单位秒
    private $value = 0; // 产生的结果值
    private $callback; // 异步回调
    private $isEnd = false; // 当前定时器任务是否结束
    public function __construct($timer,callable $callback)
    {
        $this->start = time();
        $this->timer = $timer;
        $this->callback = $callback;
    }
    public function run() {
        if($this->valid()) {
            $callback = $this->callback;
            $callback($this->value ++,$this);
            $this->start = time();
        }
    }
    /**
     * 定时执行检查
     */
    public function valid() {
        $end = time();
        if($end - $this->start >= $this->timer) {
            return true;
        } else {
            return false;
        }
    }
    public function setEnd($isEnd) {
        $this->isEnd = $isEnd;
    }
    public function getEnd() {
        return $this->isEnd;
    }
}

/**
 * 模拟阻塞的协程1
 *
 */
function taskObject1() {
    $timer = new timer(1,function($value,timer $timer) {
        if($value >= 5) {
            $timer->setEnd(true);
        }
        echo '<br>'.'A '.$value;
    });
    $tid = (yield getTaskId());
    while (true) {
        if($timer->getEnd() == true) {
            break;
        }
        yield $timer->run();
    }
}
/**
 * 模拟阻塞的协程2
 *
 */
function taskObject2() {
    $timer = new timer(2,function($value,timer $timer) {
        if($value >= 3) {
            $timer->setEnd(true);
        }
        echo '<br>'.'B '.$value;
    });
    $tid = (yield getTaskId());
    while (true) {
        if($timer->getEnd() == true) {
            break;
        }
        yield $timer->run();
    }
}
$scheduler = new Scheduler;
$scheduler->newTask(taskObject1());
$scheduler->newTask(taskObject2());
$scheduler->run();

以上实现的是:

  1. 产生两个任务,并行执行,并且给每个任务在执行的时候模拟几秒钟的阻塞;
  2. 让协程切换的时候能顺利切换,其中的任务阻塞不相互影响;

思考:

我为什么要做以上这件事情呢?因为我发现协程实现虽然很强大也很有意思,能让多任务并行,但是我在其中一个任务里调用系统函数 sleep() 的时候,阻塞任务会阻止协程切换,其实从协程的实现原理上来说也是这么回事。

那么,我也就想模拟协程阻塞,但是不产生阻塞看是否可行。PHP本身只提供了生成器为协程调用提供了支撑,如果不依赖扩展,没有提供多线程的程序实现方式,没有java那么强大,可以开子线程进行实现。

我印象中java的子线程是独立执行且不会相互阻塞的,所以我在想,PHP既然可以实现类似于多线程这样的机制,那么能不能实现调用过程中非阻塞呢?

经过这样一个实现和思考,一开始是陷入了一个误区的,是由于PHP原生函数 sleep() 阻塞造成的思维误区,那就是认为要想真正实现非阻塞或者说实现异步的话,是必须依赖于语言底层的。

后来,我想明白了一个道理,既然某个方法或者函数在执行过程中,会产生阻塞,那么把当前这个方法换成自定义的,做成非阻塞(相对于整个协程调度来说)不就行了吗?比如上面的定时执行我自己实现了一个。

而另一方面,协程调度本身的目的也是为了把任务执行过程切成尽量小片,从而快速切换执行,达到并行的目的。从这方面来看,协程应该也算是一种程序设计思想。

以下是一个程序切成尽量小片执行的例子:

// 一个简单的例子
<?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";
}

这个例子是把原本用 range 生成一个很大的整型数组的方式切换为分片执行,也就是说在遍历的时候再去取到指定的值,从代码上来看,内存消耗相对于之前来说就非常小了。


Kumfo 的杂货铺
做一些经验总结和一些学习心得分享,主要围绕PHP。 现在正在学习机器学习,会增加一些机器学习的思考分享。
avatar
kumfo
SegmentFault 后端工程师

程序生存法则:

6.5k 声望
4.1k 粉丝
0 条评论
推荐阅读
Elasticsearch 按照标签匹配个数优先排序查询
首先最外层的数组就是我们通常写的query语句,放在body中进行请求的,主要看query里面的结构,这种需要自定义脚本处理评分的,query中只放了一个script_score:

kumfo2阅读 725

怎样用 PHP 来实现枚举?
在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。枚举是一个被命名的整型常数的集合,枚举在日常生活中很常见,...

唯一丶25阅读 6.4k评论 4

PHP转Go实践:xjson解析神器「开源工具集」
我和劲仔都是PHP转Go,身边越来越多做PHP的朋友也逐渐在用Go进行重构,重构过程中,会发现php的json解析操作(系列化与反序列化)是真的香,弱类型语言的各种隐式类型转换,很大程度的减低了程序的复杂度。

王中阳Go10阅读 2.1k评论 3

封面图
图片防盗链破解 解决图片防盗链问题 反向代理
当客户端(浏览器)向服务器请求内容的时候,会提交一个header,这个header中包含了如:浏览器信息、cookie等内容,那么有一个叫referer的东东,也包含在这里面。

TANKING7阅读 11.3k评论 5

Git操作不规范,战友提刀来相见!
年终奖都没了,还要扣我绩效,门都没有,哈哈。这波骚Git操作我也是第一次用,担心闪了腰,所以不仅做了备份,也做了笔记,分享给大家。问题描述小A和我在同时开发一个功能模块,他在优化之前的代码逻辑,我在开...

王中阳Go5阅读 2.4k评论 2

封面图
Hyperf 3.0 发布,PHP 新时代
在过去的一年半时间里,Hyperf 2.2 共发布了 35 个小版本,使 Hyperf 达到了一个前所未有的高度,这里也获得了一些不错的数据反馈。

huangzhhui4阅读 1.1k评论 1

封面图
微信公众号开发:自动回复文本/图片/图文消息/关键词回复/上传素材/自定义菜单
对接流程1、申请微信公众号测试账号URL:[链接]2、登录,配置开发者服务器URL和Token开发者服务器配置代码:config.php {代码...} URL是config.php在你服务器的URLToken是上面代码自己设置的Token搞定之后,就能完...

TANKING2阅读 10.2k

avatar
kumfo
SegmentFault 后端工程师

程序生存法则:

6.5k 声望
4.1k 粉丝
宣传栏