基于 Swoole 构建高性能 Laravel 应用系列 —— 基于 Swoole 实现协程篇(一):基本概念和底层原理

栏目: PHP · 发布时间: 5年前

内容简介:协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换,相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低,Swoole 可以为每一个请求创建对应的协程,根据 IO 的状态来合理的调度协程。在 Swoole 4.x 中,协程(Coroutine)取代了异步回调,成为 Swoole 官方推荐的编程方式。Swoole 协程解决了异步回调编程困难的问题,使用协程可以以传统同步编程的方法编写代码,底层自动切换为异步 IO,既保证了编程的简单性,又可借助异步 IO,提升系统的并发能力。注

协程是什么

协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换,相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低,Swoole 可以为每一个请求创建对应的协程,根据 IO 的状态来合理的调度协程。

在 Swoole 4.x 中,协程(Coroutine)取代了异步回调,成为 Swoole 官方推荐的编程方式。Swoole 协程解决了异步回调编程困难的问题,使用协程可以以传统同步编程的方法编写代码,底层自动切换为异步 IO,既保证了编程的简单性,又可借助异步 IO,提升系统的并发能力。

注:Swoole 4.x 之前的版本也支持协程,不过 4.x 版本对协程内核进行了重构,功能更加强大,提供了完整的协程+通道特性,带来全新的 CSP 编程模型,后续介绍和示例都是基于 Swoole 4.x 版本。

基本使用示例

  • PHP 版本要求:>= 7.0;
  • 基于 ServerHttp\ServerWebSocket\Server 进行开发的时候,Swoole 底层会在 onRequestonReceiveonConnect 等事件回调之前自动创建一个协程,在回调函数中即可使用协程 API;
  • 你也可以使用 Coroutine::creatego 方法创建协程,在创建的协程中使用协程 API 进行编程。

以 Swoole 自带的 TCP 服务器 Swoole\Server 实现为例,我们可以定义服务器端实现如下:

$server = new \Swoole\Server("127.0.0.1", 9501);

// 调用 onReceive 事件回调函数时底层会自动创建一个协程
$server->on('receive', function ($serv, $fd, $from_id, $data) {
    // 向客户端发送数据后关闭连接(在这里面可以调用 Swoole 协程 API)
    $serv->send($fd, 'Swoole: ' . $data);
    $serv->close($fd);
});

$server->start();

然后我们以协程方式实现 TCP 客户端如下:

// 通过  go  函数创建一个协程
go(function () {
    $client = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP);
    // 尝试与指定 TCP 服务端建立连接,这里会触发 IO 事件切换协程,交出控制权让 CPU 去处理其他事情
    if ($client->connect("127.0.0.1", 9501, 0.5)) {
        // 建立连接后发送内容
        $client->send("hello world\n");
        // 打印接收到的消息(调用 recv 函数会恢复协程继续处理后续代码,比如打印消息、关闭连接)
        echo $client->recv();
        // 关闭连接
        $client->close();
    } else {
        echo "connect failed.";
    }
});

底层实现原理

我们以 MySQL 连接查询为例,对 Swoole 协程底层实现做一个简单的介绍:

$server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE);

#1
$server->on('Request', function($request, $response) {
    $mysql = new Swoole\Coroutine\MySQL();
    #2
    $res = $mysql->connect([
        'host' => '127.0.0.1',
        'user' => 'root',
        'password' => 'root',
        'database' => 'test',
    ]);
    #3
    if ($res == false) {
        $response->end("MySQL connect fail!");
        return;
    }
    $ret = $mysql->query('show tables', 2);
    $response->end("swoole response is ok, result=".var_export($ret, true));
});

$server->start();

在这段代码中,我们启动一个基于 Swoole 实现的 HTTP 服务器监听客户端请求,如果有 onRequest 事件发生,则通过基于 Swoole 协程实现的异步 MySQL 客户端组件对 MySQL 服务器发起连接请求,并执行查询操作,然后将结果以响应方式返回给 HTTP 客户端,下面我们来看一下协程在这段代码中的应用:

  • 调用 Swoole\Http\ServeronRequest 事件回调函数时,底层会调用 C 函数 coro_create 创建一个协程( #1 位置),同时保存这个时间点的 CPU 寄存器状态和 ZendVM 堆栈信息;
  • 调用 mysql->connect 时会发生 IO 操作,底层会调用 C 函数 coro_save 保存当前协程的状态,包括 ZendVM 上下文以及协程描述信息,并调用 coro_yield 让出程序控制权,当前的请求会挂起( #2 位置);
  • 协程让出程序控制权后,会继续进入 HTTP 服务器的事件循环处理其他事件,这时 Swoole 可以继续去处理其他客户端发来的请求;
  • 当数据库 IO 事件完成后,MySQL 连接成功或失败,底层调用 C 函数 coro_resume 恢复对应的协程,恢复 ZendVM 上下文,继续向下执行 PHP 代码( #3 位置);
  • mysql->query 的执行过程与 mysql->connect 一样,也会触发 IO 事件并进行一次协程切换调度;
  • 所有操作完成后,调用 end 方法返回结果,并销毁此协程。

注:更深层次的协程底层实现可以参考 Swoole 官方文档 的介绍。

上面这段代码我们借助了 Swoole 实现的协程 MySQL 客户端(Swoole 还提供了很多其他协程客户端,如 Redis 、HTTP等,后面我们会详细介绍),所有的编码和之前编写同步代码时并没有任何不同,但是 Swoole 底层会在 IO 事件发生时,保存当前状态,将程序控制权交出,以便 CPU 处理其它事件,当 IO 事件完成时恢复并继续执行后续逻辑,从而实现异步 IO 的功能,这正是协程的强大之处,它可以让服务器同时可以处理更多请求,而不会阻塞在这里等待 IO 事件处理完成,从而极大提高系统的并发性。

协程的适用场景

通过上面这个简单的示例,我们得出协程非常适合并发编程,常见的并发编程场景如下:

  • 高并发服务,如秒杀系统、高性能 API 接口、RPC 服务器,使用协程模式,服务的容错率会大大增加,某些接口出现故障时,不会导致整个服务崩溃;
  • 爬虫,可实现非常强大的并发能力,即使是非常慢速的网络环境,也可以高效地利用带宽;
  • 即时通信服务,如 IM 聊天、游戏服务器、物联网、消息服务器等等,可以确保消息通信完全无阻塞,每个消息包均可即时地被处理。

协程引入的问题

协程再为我们带来便利的同时,也引入了一些新的问题:

  • 协程需要为每个并发保存栈内存并维护对应的虚拟机状态,如果程序并发很大可能会占用大量内存;
  • 协程调度会增加额外的一些 CPU 开销。

尽管如此,在处理高并发应用时,使用协程带来的优势还是远远高于 PHP 默认的同步阻塞机制。

协程 vs 线程

Swoole 的协程在底层实现上是单线程的,因此同一时间只有一个协程在工作,协程的执行是串行的,这与线程不同,多个线程会被操作系统调度到多个 CPU 并行执行。

一个协程正在运行时,其他协程会停止工作。当前协程执行阻塞 IO 操作时会挂起,底层调度器会进入事件循环。当有 IO 完成事件时,底层调度器恢复事件对应的协程的执行。

在 Swoole 中对 CPU 多核的利用,仍然依赖于 Swoole 引擎的多进程机制。

协程 vs 生成器

一些框架中会使用 PHP 的 生成器 来实现半自动化的协程,但在实际使用中,开发者需要在涉及协程逻辑的函数调用前增加 yield 关键字,这带来了额外的学习和维护成本,非常容易犯错,此外 Yield/Generator 代码风格与传统的同步风格代码存在冲突,无法复用已有代码。

Swoole 协程是全自动化的协程,开发者无需添加任何关键字,底层自动实现协程的切换和调度,此外,Swoole 协程风格与传统的同步风格代码是一致的,因此可以复用已有代码。

使用时的注意事项

编程范式

  • 协程之间通讯不要使用全局变量或者引用外部变量到当前作用域,而要使用 Channel (后面会介绍具体使用)
  • 项目中如果有扩展 hook 了 zend_execute_ex 或者 zend_execute_internal 这两个函数,需要特别注意一下 C 栈,可以使用 co::set 重新设置 C 栈大小

扩展冲突

由于某些跟踪调试的 PHP 扩展大量使用了全局变量,可能会导致 Swoole 协程发生崩溃,请关闭这些相关扩展:

  • xdebug
  • phptrace
  • aop
  • molten
  • xhprof
  • phalcon(Swoole协程无法运行在 phalcon 框架中)

严重错误

由于多个协程是并发执行的,所以以下行为可能会导致协程出现严重错误:

  • 不能使用类静态变量/全局变量保存协程上下文内容,否则可能导致变量被污染,要使用 Context 管理上下文
  • 同一时间可能会有很多个请求在并行处理,多个协程共用一个客户端连接的话,就会导致不同协程之间发生数据错乱

错误和异常处理

在协程编程中可直接使用 try/catch 处理异常,但必须在协程内捕获,不得跨协程捕获异常。

此外,如果在协程内使用 exit 终止程序执行退出当前协程的话,会抛出 Swoole\ExitException 异常,你可以在需要的位置捕获该异常并实现与原生 PHP 一样的退出逻辑:

go(function () {
    try {
        Swoole\Coroutine::sleep(1);  // 模拟 IO 事件让出控制权
        exit(SWOOLE_EXIT_IN_COROUTINE);
    } catch (Swoole\ExitException $exception) {
        assert($exception->getStatus() === 1);
        assert($exception->getFlags() === SWOOLE_EXIT_IN_COROUTINE);
        return;
    }
});

注:不能将 go 函数放到 try 语句块中,这样就是跨协程捕获异常了。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

重新定义团队:谷歌如何工作

重新定义团队:谷歌如何工作

拉兹洛·博克 / 宋伟 / 中信出版集团 / 2015-12-1 / CNY 56.00

谷歌首席人才官拉斯洛•博克权威力作,谷歌公开认可的谷歌高层作品,首度揭秘谷歌颠覆工业时代模式的人才和团队管理的核心法则,《纽约时报》畅销榜第一名,Business Insider 2015最佳商业书籍,谷歌的创造力就在于此! 编辑推荐! 1、 谷歌人才官首次公开谷歌人才和团队管理的核心秘籍 在谷歌执掌人事多年的拉斯洛•博克是人才和团队管理的顶级专家。他加入谷歌后,谷歌的员工数从六......一起来看看 《重新定义团队:谷歌如何工作》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

html转js在线工具
html转js在线工具

html转js在线工具