PHP⾯试遇到⾯试官的swoole协程三连问,快哭了!
什么是进程?
进程就是应⽤程序的启动实例。独⽴的⽂件资源,数据资源,内存空间。
什么是线程?
线程属于进程,是程序的执⾏者。⼀个进程⾄少包含⼀个主线程,也可以有更多的⼦线程。线程有两种调度策略,⼀是:分时调度,⼆是:抢占式调度。
什么是协程?
协程是轻量级线程,协程也是属于线程,协程是在线程⾥执⾏的。协程的调度是⽤户⼿动切换的,所以⼜叫⽤户空间线程。协程的创建、切换、挂起、销毁全部为内存操作,消耗是⾮常低的。协程的调度策略是:协作式调度。
Swoole 协程的原理
Swoole4 由于是单线程多进程的,同⼀时间同⼀个进程只会有⼀个协程在运⾏。
Swoole server 接收数据在 worker 进程触发 onReceive 回调,产⽣⼀个携程。Swoole 为每个请求创建对应携程。协程中也能创建⼦协程。
协程在底层实现上是单线程的,因此同⼀时间只有⼀个协程在⼯作,协程的执⾏是串⾏的。
因此多任务多协程执⾏时,⼀个协程正在运⾏时,其他协程会停⽌⼯作。当前协程执⾏阻塞 IO 操作时会挂起,底层调度器会进⼊事件循环。当有 IO 完成事件时,底层调度器恢复事件对应的协程的执⾏。。所以协程不存在 IO 耗时,⾮常适合⾼并发 IO 场景。(如下图)
Swoole 的协程执⾏流程
协程没有 IO 等待 正常执⾏ PHP 代码,不会产⽣执⾏流程切换
协程遇到 IO 等待 ⽴即将控制权切,待 IO 完成后,重新将执⾏流切回原来协程切出的点
协程并⾏协程依次执⾏,同上⼀个逻辑
协程嵌套执⾏流程由外向内逐层进⼊,直到发⽣ IO,然后切到外层协程,⽗协程不会等待⼦协程结束
协程的执⾏顺序
先来看看基础的例⼦:
go(function(){
echo"hello go1 \n";
});
echo"hello main \n";
go(function(){
echo"hello go2 \n";
});
go() 是 \Co::create() 的缩写, ⽤来创建⼀个协程, 接受 callback 作为参数, callback 中的代码, 会在这个新建的协程中执⾏.
备注: \Swoole\Coroutine 可以简写为 \Co
上⾯的代码执⾏结果:
root@b98940b00a9b /v/w/c/p/swoole# php co.php
hello go1
hello main
hello go2
执⾏结果和我们平时写代码的顺序, 好像没啥区别. 实际执⾏过程:
运⾏此段代码, 系统启动⼀个新进程
遇到 go(), 当前进程中⽣成⼀个协程, 协程中输出 heelo go1, 协程退出
进程继续向下执⾏代码, 输出 hello main
再⽣成⼀个协程, 协程中输出heelo go2, 协程退出
运⾏此段代码, 系统启动⼀个新进程. 如果不理解这句话, 你可以使⽤如下代码:
/
/ co.php
<?php
sleep(100);
执⾏并使⽤ ps aux 查看系统中的进程:
root@b98940b00a9b /v/w/c/p/swoole# php co.php &
root@b98940b00a9b /v/w/c/p/swoole# ps aux
PID USER TIME COMMAND
1 root      0:00 php -a
10 root      0:00 sh
19 root      0:01 fish
749 root      0:00 php co.php
760 root      0:00 ps aux
我们来稍微改⼀改, 体验协程的调度:
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等待(IO后⾯会细讲). 执⾏的结果如下:
root@b98940b00a9b /v/w/c/p/swoole# php co.php
hello main
hello go2
hello go1
怎么不是顺序执⾏的呢? 实际执⾏过程:
运⾏此段代码, 系统启动⼀个新进程
遇到 go(), 当前进程中⽣成⼀个协程
协程中遇到 IO阻塞 (这⾥是 Co::sleep() 模拟出的 IO等待), 协程让出控制, 进⼊协程调度队列
进程继续向下执⾏, 输出 hello main
执⾏下⼀个协程, 输出 hello go2
之前的协程准备就绪, 继续执⾏, 输出 hello go1
到这⾥, 已经可以看到 swoole 中 协程与进程的关系, 以及 协程的调度, 我们再改⼀改刚才的程序:
go(function(){
Co::sleep(1);
echo"hello go1 \n";
});
echo"hello main \n";
go(function(){
Co::sleep(1);
echo"hello go2 \n";
php如何运行代码});
我想你已经知道输出是什么样⼦了:
root@b98940b00a9b /v/w/c/p/swoole# php co.php
hello main
hello go1
hello go2
协程快在哪? 减少IO阻塞导致的性能损失
⼤家可能听到使⽤协程的最多的理由, 可能就是 协程快. 那看起来和平时写得差不多的代码, 为什么就要快⼀些呢? ⼀个常见的理由是, 可以创建很多个协程来执⾏任务, 所以快. 这种说法是对的, 不过还停留在表⾯.
⾸先, ⼀般的计算机任务分为 2 种:
CPU密集型, ⽐如加减乘除等科学计算
IO 密集型, ⽐如⽹络请求, ⽂件读写等
其次, ⾼性能相关的 2 个概念:
并⾏: 同⼀个时刻, 同⼀个 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";
root@b98940b00a9b /v/w/c/p/swoole# time 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";
root@b98940b00a9b /v/w/c/p/swoole# time 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";
root@b98940b00a9b /v/w/c/p/swoole# time 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就绪时恢复运⾏
我们将多协程版稍微修改⼀下:
多协程版2: 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";
root@b98940b00a9b /v/w/c/p/swoole# time 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

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。