二十九、Swoole 基础学习笔记 - Swoole 初窥协程

作者: 温新

分类: 【Swoole 系列】

阅读: 1898

时间: 2023-03-13 12:09:31

hi,我是温新,一名PHPer

文章基于 Swoole 5.0.1 版本编写。

学习目标:了解什么是协程,协程可以用来干嘛

说明:本篇文章结合官方文档编写及参考网络资料编写,虽非全部原创,但也是结合了自己的理解,若转载请附带本文 URL,编写不易,持续编写更不易,谢谢!

什么是协程

协程(Coroutine)可以理解为纯用户态的线程,它是通过协作而不是抢占来进行切换。相对于进程或线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。

Swoole 可以为每一个请求创建对应的协程,根据 IO 的状态合理的调度协程。

Swoole 协程解决了异步回调编程困难的问题,使用协程可以以传统的方法编写代码,底层自动切换为异步 IO,既保证了编程的简单性,又可以 借助异步 IO 提升系统的并发能力。

协程调度器只可以在协程 A 即将进入阻塞 IO 操作时,将该协程挂起,把当前的栈信息 Stack A 保存起来,然后切换到协程 B,等到协程 A 的该 IO 操作返回时,再根据 Stack A 切回到之前的协程 A 当时的状态。协程相对于事件驱动是一种更先进的并发解决方案,把复杂的逻辑和异步都封在底层,让程序员在编程时感觉不到异步的存在。

为什么需要协程

有了多线程,为什么还需要协程?因为多线和存在如下几个问题:

  • 线程内存占用;
  • 线程之间彼此相关竞争;
  • 线程之间切换耗时。

这些原因不详细介绍了,自己查询资料吧。

为什么要使用协程

协程解决了线程所存在的问题。为什么使用协程有如下原因:

  • **节省 CPU,避免系统内核级的线程频繁切换,而造成 CPU 资源浪费。**协程是用户态的线程,用户可以自行控制协程的创建与销毁,极大程度避免了系统线程上下文切换造成的资源浪费;
  • **节约内存,在 64 位 Linux 系统中,一个线程需要分配 8M 栈内存和 64M 堆内存,系统内存的制约导致无法开启更多线程实现高并发。**而在协程模式下,可以轻松有十几万协程,这是线程无法比拟的;
  • 稳定性,线程之间通过内存共享数据,这就导致了一个问题,任何一个线程出错时,进程中的所有线程都会跟着一起崩溃;
  • 开发效率,使用协程进行程序开发,可以很方便的将一些耗时的 IO 操作异步化,如写文件、耗时 IO 请求等。

协程的优势

1、开发者可以无感知的使用同步代码编写的方式达到异步 IO 的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码维护困难;

2、由于 Swoole 是在底层封装了协程,所以对比传统的 PHP 层协程框架,开发者不需要使用 yield 关键词来标识一个协程 IO 操作,所以不再需要对 yield 的语义进行深入理解及对每一级的调用都修改为 yield,这极大的提高了开发效率。

注意:

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

2、协程遇到 IO 才会切换,单线程遇到 IO 或执行时间过长就会被迫交出 CPU 执行权限,从而切换到其他线程运行;

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

协程的使用场景

协程非常适合并发编程,常见的并发场景如下:

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

协程所带来的问题

任何事物都有两面性,协程带来便利的同时也引入了新的问题:

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

使用协程注意事项

编程范式

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

扩展冲突

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

  • xdebug
  • phptrace
  • aop
  • molten
  • xhprof
  • phalcon

严重错误

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

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

错误和异常处理

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

若在协程中使用 exit 终止程序退出当前协程的话,会抛出 Swoole\ExitException 异常。可以在需要的位置捕获该异常并实现与 PHP 一样的退出逻辑。

通过案例体验协成

基于 ServerHttp\ServerWebSocket\Server 进行开发时,Swoole 底层会在 onRequestonReceiveonConnect 等事件回调之前自动创建一个协程,在回调函数中即可使用协程 API。

服务端/客户端协程案例

服务端:29-swoole-server-1.php

<?php
// 29-swoole-server-1.php

$server = new Swoole\Server('0.0.0.0', 9501);

$server->on('Receive', function ($server, $fd, $fromId, $data) {
    echo '来自客户端: ' . $fd . ' 的消息: ' . $data;
    $server->send($fd, 'Swoole: ' . $data);
    $server->close($fd);
});

$server->start();

协程客户端:29-swoole-coroutine-1.php

<?php
// 29-swoole-coroutine-1.php

use Swoole\Coroutine\Client;
use function Swoole\Coroutine\run;

// 协程客户端
run(function () {
    $client = new Client(SWOOLE_SOCK_TCP);
    if (! $client->connect('127.0.0.1', 9501, 0.5)) {
        echo '连接失败,错误码:' . $client->errCode . PHP_EOL;
    }

    $client->send("hello world\n");
    echo $client->recv();
    $client->close();
});

执行服务

$php 29-swoole-http-server-1.php 
来自客户端: 1 的消息: hello world

$php 29-swoole-coroutine-1.php 
Swoole: hello world

协程 MySQL

<?php
// 29-swoole-coroutine-2-mysql.php
use Swoole\Coroutine\MySQL;
$server=new swoole\http\server("0.0.0.0",9503);

//swoole会开辟一个协程栈,对协程栈进行初始化
$server->on('request',function($request, $response){
    $response->header('Content-Type', 'text/html; charset=utf-8');
    $mysql = new MySQL();
    $res = $mysql->connect([
        'host' => '127.0.0.1',
        'port' => 3306,
        'user' => 'root',
        'password' => '123456',
        'database' => 'test1',
    ]);

    if ($res == false) {
        $response->end('connect is fail');
        return;
    }

    $ret = $mysql->query('show tables');

    $response->end('swoole is ok, result = ' . var_export($ret, true));
});

$server->start();

本篇文章学习了协程的概念及案例体验,那么本篇文章就结束了,我们下篇文章继续学习。

请登录后再评论