小艾的自留地

Stay foolish, Stay hungry

最近有幸读到 daydaygoswoole 协程初体验,一文从协程的执行的角度窥探 Swoole 的协程调度,并详细说明了为什么协程会快。

文章通俗易懂,笔者在此基础上增加了一些自己的理解,以此成文。

主要从以下两个方面来了解协程:

  1. 协程的执行顺序:协程调度
  2. 协程为什么快:减少IO阻塞带来的性能优势

协程执行顺序

按照惯例,先来看一个最简单的协程代码。

1
2
3
4
5
6
7
8
9
10
<?php
go(function () {
echo "1".PHP_EOL;
});

echo "2".PHP_EOL;

go(function () {
echo "3".PHP_EOL;
});

在Swoole中 Swoole\Coroutine::create 等价于 go 函数(Swoole\Coroutine 前缀的类名可以映射为 Co),用于创建一个协程。

该函数接受一个回调函数作为参数,回调函数的内容就是协程需要执行的内容。

上面的代码执行结果为:

1
2
3
1
2
3

从执行结果的角度来看,协程版的代码和传统的同步代码,看起来并无差异。但协程的实际执行过程却是:

  1. 运行上面那段协程代码,生成一个新进程
  2. 当代码执行到go()部分时,会在当前协程中创建一个协程,输出1,协程退出
  3. 代码继续向下执行,输出 2
  4. 再次遇到go()函数,输出3
  5. 协程退出,进程退出,执行完成

协程调度

\Co::sleep() 函数和sleep()函数差不多,但是它模拟的是 IO 等待。

1
2
3
4
5
6
7
8
9
10
11
12
<?php
go(function () {
// 只新增了一行代码
Co::sleep(1);
echo "1".PHP_EOL;
});

echo "2".PHP_EOL;

go(function () {
echo "3".PHP_EOL;
});

执行结果如下:

1
2
3
2
3
1

怎么不是顺序执行的了?实际执行过程:

  1. 运行上面那段协程代码,生成一个新进程
  2. 遇到 go(),在当前进程中创建一个协程
  3. 协程向下执行遇到IO 阻塞,协程让出控制,进入协程调度队列
  4. 进程继续向下执行,输出 2
  5. 创建第二个协程,输出3
  6. 第一个协程准备就绪,输出 1
  7. 协程退出,进程退出,执行完成

到这里,已经可以看到Swoole 中协程进程的关系,以及协程调度的过程。

下面这张图可以很清晰的看到二者区别与联系:

协程快在哪里?

大家使用协程,听到最多的原因,可能就是因为协程快。那协程相比传统同步代码倒底快在哪里呢?

首先,我们来了解一下计算机中的两类任务。

CPU密集型

CPU 密集型也叫计算密集型, 特点是需要进行大量科学计算,比如计算圆周率、对视频进行高清解码,吃CPU。

IO 密集型

涉及到网络、磁盘IO的任务都是IO密集型任务,特点是不吃CPU,任务的大部分时间都在等待IO操作完成,因为IO的速度远远低于CPU和内存的速度。

其次需要了解两个概念:

  • 并行:同一时刻,同一CPU只能执行一个任务,要N个任务同时执行,就需要有多个CPU 才行。
  • 并发:同一时刻执行N 个任务。由于CPU 任务切换速度非常快,已经快到了人类感知极限。

了解了这些基础之后,对协程的能力是不是也更清晰了一些,以及协程为什么会“快”了。

因为协程仅在 IO阻塞 时才会触发调度,从而减少等待IO 操作完成的时间。

协程实践

通过对比下面三种情况,加深对协程的理解:

同步阻塞版:

1
2
3
4
5
6
7
<?php
$n = 4;
for ($i = 0; $i < $n; $i++) {
sleep(1);
echo $i . PHP_EOL;
};
echo "ok";

单个协程版:

1
2
3
4
5
6
7
8
9
<?php
$n = 4;
Co\Run(function () use ($n) {
for ($i = 0; $i < $n; $i++) {
Co::sleep(1);
echo $i . PHP_EOL;
};
});
echo "ok";

多个协程版1.0(IO 密集型):

1
2
3
4
5
6
7
8
9
<?php
$n = 4;
for ($i = 0; $i < $n; $i++) {
go(function () use ($i) {
Co::sleep(1);
echo $i . PHP_EOL;
});
};
echo "ok";

通过 time 命令分别查看耗时时长,可以得出以下结论:

  • 传统同步阻塞:遇到 IO阻塞,等待,导致性能损失
  • 单协程:尽管 IO阻塞引发了协程调度,但有且只有一个协程
  • 多协程:遇到 IO阻塞 时发生调度,IO就绪时恢复运行

多个协程版2.0(CPU 密集型):

1
2
3
4
5
6
7
8
9
10
<?php
$n = 4;
for ($i = 0; $i < $n; $i++) {
go(function () use ($i) {
sleep(1);
// Co::sleep(1);
echo $i . PHP_EOL;
});
};
echo "ok";

只是将 Co::sleep() 改成了 sleep(),会发现总耗时时长又和传统同步阻塞差不多了,这是因为:

  • sleep() 可以看做是 CPU密集型任务, 不会引起协程的调度
  • Co::sleep() 模拟的是 IO密集型任务, 会引发协程的调度

这也是为什么, 协程适合 IO密集型 的应用,而不适合 CPU 密集型任务。

参考链接

评论