浅谈 Swoole 的协程

PHP 对于异步编程的功能的缺失真的是一大痛点,之前有 Facebook 的 hhvm 可以使用,但是现在 hhvm 都不兼容PHP了,很多人都说 PHP7 的性能跟 hhvm 已经差不多了,使用 hhvm 没有意义了,说这话的人估计连使用 hack 写 hello,world 都没写过。hhvm 添加了 async await 异步编程,collection 统一了数组操作,添加了泛型,这些都是PHP的最大的诟病。

不过现在 swoole 至少解决了PHP无法实现高性能网络编程这个问题,虽然它的协程功能还是比较简陋的,但是用起来也是足够了。 使用关键字go就能够创建一个协程:

<?php

use Swoole\Coroutine as Co;

go(function () {
    Co::sleep(5);
    echo "hello swoole";
});

在这里会有几个问题:

  1. go 函数传入的是一个函数,那么假如我的下一个协程的参数依赖于上一个协程的返回结果,那么该如何实现?
  2. Co::sleep 使用sleep替代是否效果一样?

在并发编程中通常会涉及到两种主要的编程模型,一种是CSP模型,另外一种是Actors模型。

CSP(Communicating Sequential Process)模型提供一种多个协程公用的“管道(channel)”, 这个channel中存放的是一个个”任务”,多个协程之间通过channel进行通信,channel相当于FIFO queue。

Actors模型更多的使用消息机制来实现并发,目标是让开发者不再考虑线程这种东西,每个Actor最多同时只能进行一样工作,Actor内部可以有自己的变量和数据。在Actors模型中,每个Actor都有一个专属的命名“mailbox”, 其他Actor可以随时选择一个Actor通过mailbox收发数据,对于“mailbox”的维护,通常是使用发布订阅的机制实现的。

因为CSP模型比较简单,所以很多编程语言都提供了CSP编程的功能,比如python的asyncio,swoole,lua,golang,julia 。Actors 模型比较复杂,相应的功能也比较强大,erlang 就是使用actors,可以非常容易地实现分布式编程。

通常在我们的 Web 架构中,需要处理一些导入导出,或者发送邮件,发送短信功能的时候,我们会引入消息队列,像 redis,rabbitmq,这样我们可以把复杂耗时的任务丢给后台,前台的页面就不会一直转圈圈了。那么可以把协程想象成一个 web 应用,channel 就是消息队列,不同的应用之间通过消息队列进行通信,协程就是通过 channel 进行消息传递。

第一个问题当下一个协程依赖于上一个协程的返回结果时,像 python 可以通过 async await 语法轻松实现,但是 swoole 没有提供,但是可以使用 channel 来实现,把上一个协程的结果放入channel中,下一个协程监听这个channel就可以了。这种情况会在写类似爬虫的时候会经常遇到,比如我们需要同步地抓取20个地址页面,但是我们需要统一处理这20个页面的返回值,相当于nodejs 的 Promise.all:

<?php
/**
 * 首先确保所执行的函数体里面没有阻塞的函数,例如PDO,file_get_contents等等,
 * 如果有这些函数,请先执行: Swoole\Runtime::enableCoroutine();
 */

use Swoole\Coroutine as Co;

class GroupWait
{
    private $channel;

    private $coList;

    private $len;

    public function __construct(array $coList)
    {
        $this->coList = $coList;
        $this->len = count($coList);
        $this->channel = new Co\channel($this->len);
    }

    public function wait()
    {
        foreach ($this->coList as $key => $co) {
            go(function () use ($key, $co) {
                $data = $co();
                $this->channel->push([$key, $data]);
            });
        }

        $result = [];
        for($i = 1; $i <= $this->len; $i++) {
            list($k, $v) = $this->channel->pop();
            $result[$k] = $v;
        }

        return $result;
    }

    public function __destruct()
    {
        $this->channel->close();
    }
}

第二个问题,使用sleep会阻塞主进程,而 Co::sleep 不会阻塞主进程,它阻塞的是自身的协程,所以在使用 swoole 异步编程的时候,像 get_file_contents, PDO, MySQL ,文件操作都是阻塞主进程的,所以有些人使用 swoole 还是会很慢,像这些操作要么使用 swoole 提供的方法,比如 swoole的 MySQL 和 Redis 驱动,或者开启 runtime: Swoole\Runtime::enableCoroutine()