swoole 协程介绍
什么是协程:
协程可以简单理解为线程,只不过这个线程是用户态的,不需要操作系统参与,创建销毁和切换的成本非常低,和线程不同的是协程没法利用多核 cpu 的,想利用多核 cpu 需要依赖 Swoole 的多进程模型。
什么是 channel:
channel 可以理解为消息队列,只不过是协程间的消息队列。多个协程通过 push 和 pop 操作生产消息和消费消息,用来协程之间的通讯。需要注意的是 channel 是无法跨进程的,只能一个 Swoole 进程里的协程间通讯,最典型的应用是连接池或并发调用。
协程的执行顺序:
go(function () { echo "hello go1 \n"; }); echo "hello main \n"; go(function () { echo "hello go2 \n"; });
go() 是 \Co::create() 的缩写,用来创建一个协程,接受callback作为参数,callback中的代码。会在这个新建的协程中执行。
备注:\Swoole\Coroutine 可以简写为 \Co
上面的代码执行结果:
# php co.php hello go1 hello main hello go2
实际执行过程:
- 运行此段代码,系统启动一个新进程
- 遇到 go() ,当前进程中生成一个协程,协程中输出 hello go1,协程退出
- 进程继续向下执行代码,输出 hello main
- 再生成一个协程,协程中输出 hello go2,协程退出
下面稍微改一下执行顺序
use Co; go(function () { Co::sleep(1); // 只新增了一行代码 echo "hello go1 \n"; }); echo "hello main \n"; go(function () { echo "hello go2 \n"; });
\Co::sleep() 函数功能和 sleep() 差不多,但是它模拟的是IO等待,执行的顺序如下:
# php co.php hello main hello go2 hello go1
上面的实际执行过程如下:
- 运行此段代码,系统启动一个进程
- 遇到 go(),当前进程中生成一个协程
- 协程中遇到IO阻塞(这里是 Co::sleep() 模拟出来的IO等待),协程让出控制,进入协程调度队列
- 进程继续向下执行,输出 hello main
- 执行下一个协程,输出 hello go2
- 之前的协程准备就绪,继续执行,输出 hello go1
协程快在哪?减少IO阻塞导致的性能损失
一般的计算机任务分为两种:
- CPU密集型,比如加减乘除等科学计算
- IO密集型,比如网络请求,文件读写等
高性能相关的两个概念:
- 并行:同一个时刻,同一个CPU只能执行同一个任务,要同时执行多个任务,就需要有多个CPU才行
- 并发:由于CPU切换任务非常快,所以让人感觉像是有多个任务同时执行
协程适合的场景是IO密集型应用,因为协程在IO阻塞时会自动调度,减少IO阻塞导致的时间损失。
普通版:执行4个任务
$n = 4; for ($i = 0; $i < $n; $i++) { sleep(1); echo microtime(true) . ": hello $i \n"; }; echo "hello main \n";
执行结果:
# php co.php 1528965075.4608: hello 0 1528965076.461: hello 1 1528965077.4613: hello 2 1528965078.4616: hello 3 hello main real 0m 4.02s user 0m 0.01s sys 0m 0.00s
单个协程版:
$n = 4; go(function () use ($n) { for ($i = 0; $i < $n; $i++) { Co::sleep(1); echo microtime(true) . ": hello $i \n"; }; }); echo "hello main \n";
执行结果:
# php co.php hello main 1528965150.4834: hello 0 1528965151.4846: hello 1 1528965152.4859: hello 2 1528965153.4872: hello 3 real 0m 4.03s user 0m 0.00s sys 0m 0.02s
多协程版本:
$n = 4; for ($i = 0; $i < $n; $i++) { go(function () use ($i) { Co::sleep(1); echo microtime(true) . ": hello $i \n"; }); }; echo "hello main \n";
执行结果:
# php co.php hello main 1528965245.5491: hello 0 1528965245.5498: hello 3 1528965245.5502: hello 2 1528965245.5506: hello 1 real 0m 1.02s user 0m 0.01s sys 0m 0.00s
这三种版本为什么时间上有很大的差异?
- 普通版本:会遇到IO阻塞,导致的性能损失
- 单协程版本:尽管IO阻塞引发了协程调度,但当前只有一个协程,调度之后还是执行当前协程
- 多协程版本:真正发挥出协程的优势,遇到IO阻塞时发生调度,IO就绪时恢复运行
下面将多协程版本修改为CPU密集型
$n = 4; for ($i = 0; $i < $n; $i++) { go(function () use ($i) { // Co::sleep(1); sleep(1); echo microtime(true) . ": hello $i \n"; }); }; echo "hello main \n";
执行的结果:
# php co.php 1528965743.4327: hello 0 1528965744.4331: hello 1 1528965745.4337: hello 2 1528965746.4342: hello 3 hello main real 0m 4.02s user 0m 0.01s sys 0m 0.00s
只是将 Co::sleep() 改成了sleep() ,时间又和普通版本差不多,原因是:
- sleep() 可以看做是CPU密集型任务,不会引起协程的调度
- Co::sleep() 模拟的是IO密集型任务,会引发协程的调度
这就是为什么协程适合IO密集型应用。
下面使用一组对比,使用redis:
// 同步版, redis使用时会有 IO 阻塞 $cnt = 2000; for ($i = 0; $i < $cnt; $i++) { $redis = new \Redis(); $redis->connect('redis'); $redis->auth('123'); $key = $redis->get('key'); } // 单协程版: 只有一个协程, 并没有使用到协程调度减少 IO 阻塞 go(function () use ($cnt) { for ($i = 0; $i < $cnt; $i++) { $redis = new Co\Redis(); $redis->connect('redis', 6379); $redis->auth('123'); $redis->get('key'); } }); // 多协程版, 真正使用到协程调度带来的 IO 阻塞时的调度 for ($i = 0; $i < $cnt; $i++) { go(function () { $redis = new Co\Redis(); $redis->connect('redis', 6379); $redis->auth('123'); $redis->get('key'); }); }
性能对比:
# 多协程版 # php co.php real 0m 0.54s user 0m 0.04s sys 0m 0.23s # 同步版 # php co.php real 0m 1.48s user 0m 0.17s sys 0m 0.57s
swoole协程和go协程对比:单进程 VS 多线程
package main import ( "fmt" "time" ) func main() { go func() { fmt.Println("hello go") }() fmt.Println("hello main") time.Sleep(time.Second) }
执行结果:
$ go run test.go hello main hello go
go代码的执行过程如下:
- 运行 go 代码,系统启动一个新进程
- 查找 package main ,然后执行其中的 func main()
- 遇到协程,交给协程调度器执行
- 继续向下执行,输出 hello main
- 如果不添加 time.Sleep(time.Second),main函数执行完,程序结束,进程退出,导致调度中的协程也终止
swoole和go实现协程调度的模型不同,go中使用的是MPG模型:
- M 指的是 Machine, 一个M直接关联了一个内核线程
- P 指的是 processor, 代表了M所需的上下文环境, 也是处理用户级代码逻辑的处理器
- G 指的是 Goroutine, 其实本质上也是一种轻量级的线程
而swoole中的协程调度使用单进程模型,所有协程都是在当前进程中进行调度,单进程的好处是:简单 / 不用加锁 / 性能高。