SwooleCoroutine

什么是协程

协程可以简单理解为线程,只不过这个线程是用户态的,不需要操作系统参与,创建销毁和切换的成本非常低,和线程不同的是协程没法利用多核 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Swoole\Coroutine;
use function Swoole\Coroutine\run;

echo "main start\n";
run(function () {
echo "coro " . Coroutine::getcid() . " start\n";
Coroutine::create(function () {
echo "coro " . Coroutine::getcid() . " start\n";
Coroutine::sleep(.2);
echo "coro " . Coroutine::getcid() . " end\n";
});
echo "coro " . Coroutine::getcid() . " do not wait children coroutine\n";
Coroutine::sleep(.1);
echo "coro " . Coroutine::getcid() . " end\n";
});
echo "end\n";
1
2
3
4
5
6
7
8
9
/*
main start
coro 1 start
coro 2 start
coro 1 do not wait children coroutine
coro 1 end
coro 2 end
end
*/

注意事项

在使用 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
2
3
4
5
6
7
8
9
10
11
12
13
use Swoole\Coroutine;
use Swoole\Coroutine\Http\Client;
use function Swoole\Coroutine\run;

run(function() {
$cli = new Client('www.xinhuanet.com', 80);
Coroutine::create(function () use ($cli) {
$cli->get('/');
});
Coroutine::create(function () use ($cli) {
$cli->get('/');
});
});

解决方案参考:https://wenda.swoole.com/detail/107474

此限制对于所有多协程环境都有效,最常见的就是在 onReceive 等回调函数中去共用一个 TCP 连接,因为此类回调函数会自动创建一个协程, 那有连接池需求怎么办?

Swoole 内置了连接池可以直接使用,或手动用 channel 封装连接池。