page contents

Swoole 实现协程基本概念和底层原理

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

attachments-2020-08-4XtIHk1x5f49c3fc541ae.png


协程是什么


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

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

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


基本使用示例


  • PHP 版本要求:>= 7.0;
  • 基于 Server、Http\Server、WebSocket\Server 进行开发的时候,Swoole 底层会在 onRequest、onReceive、onConnect 等事件回调之前自动创建一个协程,在回调函数中即可使用协程 API;
  • 你也可以使用 Coroutine::create 或 go 方法创建协程,在创建的协程中使用协程 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\Server 的 onRequest 事件回调函数时,底层会调用 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;
    }
});


attachments-2020-08-Uyw1hYRq5f49c411740dc.jpg

  • 发表于 2020-08-29 10:57
  • 阅读 ( 617 )

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1135 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1316 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章