什么是协程
协程可以简单理解为线程,只不过这个线程是用户态的,不需要操作系统参与,创建销毁和切换的成本非常低,和线程不同的是协程没法利用多核 CPU 的,想利用多核 CPU 需要依赖 Swoole 的多进程模型。
什么是 Channel
Channel 可以理解为消息队列,只不过是协程间的消息队列,多个协程通过 push 和 pop 操作队列中的生产消息和消费消息,用来发送或者接收数据进行协程之间的通讯。需要注意的是 Channel 是没法跨进程的,只能一个 Swoole 进程里的协程间通讯,最典型的应用是连接池和并发调用。
什么是协程容器
使用 Coroutine::create 或 go() 方法创建协程 (参考别名小节),在创建的协程中才能使用协程 API,而协程必须创建在协程容器里面,参考协程容器。
协程调度
这里将尽量通俗的讲述什么是协程调度,首先每个协程可以简单的理解为一个线程,大家知道多线程是为了提高程序的并发,同样的多协程也是为了提高并发。
用户的每个请求都会创建一个协程,请求结束后协程结束,如果同时有成千上万的并发请求,某一时刻某个进程内部会存在成千上万的协程,那么 CPU 资源是有限的,到底执行哪个协程的代码?
决定到底让 CPU 执行哪个协程的代码的决断过程就是协程调度,Swoole 的调度策略又是怎么样的呢?
- 首先,在执行某个协程代码的过程中发现这行代码遇到了 Co::sleep() 或者产生了网络 IO,例如 MySQL->query(),这肯定是一个耗时的过程,Swoole 就会把这个 MySQL 连接的 Fd 放到 EventLoop 中。
- 然后让出这个协程的 CPU 给其他协程使用:即** **yield**(挂起)**
- 等待 MySQL 数据返回后再继续执行这个协程:即** **resume**(恢复)**
- 其次,如果协程的代码有 CPU 密集型代码,可以开启 enable_preemptive_scheduler,Swoole 会强行让这个协程让出 CPU。
父子协程优先级
优先执行子协程 (即 go() 里面的逻辑),直到发生协程 yield(Co::sleep () 处),然后协程调度到外层协程。
1 | use Swoole\Coroutine; |
1 | /* |
注意事项
在使用 Swoole 编程前应该注意的地方:
全局变量
协程使得原有的异步逻辑同步化,但是在协程间的切换是隐式发生的,所以在协程切换的前后不能保证全局变量以及 static 变量的一致性。
在 PHP-FPM 下可以通过全局变量获取到的请求参数,服务器的参数等。
在 Swoole 内,无法 通过 $_GET/$_POST/$_REQUEST/$_SESSION/$COOKIE/$_SERVER 等以 $开头的变量获取到任何属性参数。
可以使用 context 用协程 id 做隔离,实现全局变量的隔离。
多协程共享 TCP 连接
对于一个 TCP 连接来说 Swoole 底层允许同时只能有一个协程进行读操作、一个协程进行写操作。也就是说不能有多个协程对一个 TCP 进行读 / 写操作,底层会抛出绑定错误:
1 | Fatal error: Uncaught Swoole\Error: Socket#6 has already been bound to another coroutine#2, reading or writing of the same socket in coroutine#3 at the same time is not allowed |
重现代码:
1 | use Swoole\Coroutine; |
解决方案参考:https://wenda.swoole.com/detail/107474
此限制对于所有多协程环境都有效,最常见的就是在 onReceive 等回调函数中去共用一个 TCP 连接,因为此类回调函数会自动创建一个协程, 那有连接池需求怎么办?
Swoole 内置了连接池可以直接使用,或手动用 channel 封装连接池。