内容简介:协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换,相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低,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;
-
基于
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; } });
注:不能将 go 函数放到 try 语句块中,这样就是跨协程捕获异常了。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 云计算底层技术之高性能集群趣谈
- avue 1.5.2 优化大量底层代码,crud 和 form 底层公用
- Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比
- Docker 底层原理浅析
- NSDictionary底层实现原理
- PHP 数组底层实现
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
重新定义团队:谷歌如何工作
拉兹洛·博克 / 宋伟 / 中信出版集团 / 2015-12-1 / CNY 56.00
谷歌首席人才官拉斯洛•博克权威力作,谷歌公开认可的谷歌高层作品,首度揭秘谷歌颠覆工业时代模式的人才和团队管理的核心法则,《纽约时报》畅销榜第一名,Business Insider 2015最佳商业书籍,谷歌的创造力就在于此! 编辑推荐! 1、 谷歌人才官首次公开谷歌人才和团队管理的核心秘籍 在谷歌执掌人事多年的拉斯洛•博克是人才和团队管理的顶级专家。他加入谷歌后,谷歌的员工数从六......一起来看看 《重新定义团队:谷歌如何工作》 这本书的介绍吧!