协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换,相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低,Swoole 可以为每一个请求创建对应的协程,根据 IO 的状态来合理的调度协程。
在 Swoole 4.x 中,协程(Coroutine)取代了异步回调,成为 Swoole 官方推荐的编程方式。Swoole 协程解决了异步回调编程困难的问题,使用协程可以以传统同步编程的方法编写代码,底层自动切换为异步 IO,既保证了编程的简单性,又可借助异步 IO,提升系统的并发能力。
注:Swoole 4.x 之前的版本也支持协程,不过 4.x 版本对协程内核进行了重构,功能更加强大,提供了完整的协程+通道特性,带来全新的 CSP 编程模型,后续介绍和示例都是基于 Swoole 4.x 版本。
以 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 官方文档的介绍。
上面这段代码我们借助了 Swoole 实现的协程 MySQL 客户端(Swoole 还提供了很多其他协程客户端,如 Redis、HTTP等,后面我们会详细介绍),所有的编码和之前编写同步代码时并没有任何不同,但是 Swoole 底层会在 IO 事件发生时,保存当前状态,将程序控制权交出,以便 CPU 处理其它事件,当 IO 事件完成时恢复并继续执行后续逻辑,从而实现异步 IO 的功能,这正是协程的强大之处,它可以让服务器同时可以处理更多请求,而不会阻塞在这里等待 IO 事件处理完成,从而极大提高系统的并发性。
通过上面这个简单的示例,我们得出协程非常适合并发编程,常见的并发编程场景如下:
协程再为我们带来便利的同时,也引入了一些新的问题:
尽管如此,在处理高并发应用时,使用协程带来的优势还是远远高于 PHP 默认的同步阻塞机制。
Swoole 的协程在底层实现上是单线程的,因此同一时间只有一个协程在工作,协程的执行是串行的,这与线程不同,多个线程会被操作系统调度到多个 CPU 并行执行。
一个协程正在运行时,其他协程会停止工作。当前协程执行阻塞 IO 操作时会挂起,底层调度器会进入事件循环。当有 IO 完成事件时,底层调度器恢复事件对应的协程的执行。
在 Swoole 中对 CPU 多核的利用,仍然依赖于 Swoole 引擎的多进程机制。
一些框架中会使用 PHP 的生成器来实现半自动化的协程,但在实际使用中,开发者需要在涉及协程逻辑的函数调用前增加 yield 关键字,这带来了额外的学习和维护成本,非常容易犯错,此外 Yield/Generator 代码风格与传统的同步风格代码存在冲突,无法复用已有代码。
Swoole 协程是全自动化的协程,开发者无需添加任何关键字,底层自动实现协程的切换和调度,此外,Swoole 协程风格与传统的同步风格代码是一致的,因此可以复用已有代码。
编程范式
扩展冲突
由于某些跟踪调试的 PHP 扩展大量使用了全局变量,可能会导致 Swoole 协程发生崩溃,请关闭这些相关扩展:
严重错误
由于多个协程是并发执行的,所以以下行为可能会导致协程出现严重错误:
在协程编程中可直接使用 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 语句块中,这样就是跨协程捕获异常了。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!