共计 4256 个字符,预计需要花费 11 分钟才能阅读完成。
第一次接触协程这个概念,是在学习 Swoole 时,那时看官网文档并不能齐全了解协程到底是个什么货色以及该如何正确的应用它。
起初逐步看了一些写的比拟艰深的文章,加上本人的一些了解,逐渐开始对协程有一些意识了。
意识协程
协程不是过程或线程,其执行过程更相似于子例程,或者说不带返回值的函数调用。
下面那句话很要害,一句话就把协程是什么,不是什么说分明了。
上面这张图能够很清晰的看到协程与多过程的区别:
执行程序
上面这段代码次要做了三件事:写入文件、发送邮件以及插入数据。
<?php | |
function task1(){for ($i=0;$i<=300;$i++){ | |
// 写入文件, 大略要 3000 微秒 | |
usleep(3000); | |
echo "写入文件{$i}\n"; | |
} | |
} | |
function task2(){for ($i=0;$i<=500;$i++){ | |
// 发送邮件给 500 名会员, 大略 3000 微秒 | |
usleep(3000); | |
echo "发送邮件{$i}\n"; | |
} | |
} | |
function task3(){for ($i=0;$i<=100;$i++){ | |
// 模仿插入 100 条数据, 大略 3000 微秒 | |
usleep(3000); | |
echo "插入数据{$i}\n"; | |
} | |
} | |
task1(); | |
task2(); | |
task3(); |
这段代码和下面不同的是,这三件事件是穿插执行的,每个工作执行完一次之后,切换到另一个工作,如此循环。
相似于这样的执行程序,就是协程。
<?php | |
function task1($i) | |
{ | |
// 应用 $i 标识 写入文件, 大略要 3000 微秒 | |
if ($i > 300) {return false;// 超过 300 不必写了} | |
echo "写入文件{$i}\n"; | |
usleep(3000); | |
return true; | |
} | |
function task2($i) | |
{ | |
// 应用 $i 标识 发送邮件, 大略要 3000 微秒 | |
if ($i > 500) {return false;// 超过 500 不必发送了} | |
echo "发送邮件{$i}\n"; | |
usleep(3000); | |
return true; | |
} | |
function task3($i) | |
{ | |
// 应用 $i 标识 插入数据, 大略要 3000 微秒 | |
if ($i > 100) {return false;// 超过 100 不必插入} | |
echo "插入数据{$i}\n"; | |
usleep(3000); | |
return true; | |
} | |
$i = 0; | |
while (true) {$task1Result = task1($i); | |
$task2Result = task2($i); | |
$task3Result = task3($i); | |
if($task1Result===false&&$task2Result===false&&$task3Result===false){break;// 全副工作实现, 退出循环} | |
$i++; | |
} |
swoole 实现协程代码:
<?php | |
function task1(){for ($i=0;$i<=300;$i++){ | |
// 写入文件, 大略要 3000 微秒 | |
usleep(3000); | |
echo "写入文件{$i}\n"; | |
Co::sleep(0.001);// 挂起以后协程,0.001 秒后复原 // 相当于切换协程 | |
} | |
} | |
function task2(){for ($i=0;$i<=500;$i++){ | |
// 发送邮件给 500 名会员, 大略 3000 微秒 | |
usleep(3000); | |
echo "发送邮件{$i}\n"; | |
Co::sleep(0.001);// 挂起以后协程,0.001 秒后复原 // 相当于切换协程 | |
} | |
} | |
function task3(){for ($i=0;$i<=100;$i++){ | |
// 模仿插入 100 条数据, 大略 3000 微秒 | |
usleep(3000); | |
echo "插入数据{$i}\n"; | |
Co::sleep(0.001);// 挂起以后协程,0.001 秒后复原 // 相当于切换协程 | |
} | |
} | |
$pid1 = go('task1');//go 函数是 swoole 的开启协程函数,用于开启一个协程 | |
$pid2 = go('task2'); | |
$pid3 = go('task3'); |
协程与多过程
由下面的代码,能够发现,协程其实只是运行在一个过程中的函数,只是这个函数会被切换到下一个执行。
须要留神的是⚠️:
协程并不是多任务并行处理,它属于多任务串行解决,它俩的本质区别是在某个时刻同时执行一个还是多个工作。
协程的作用域
因为协程就是过程中一串工作代码,所以它的全局变量、动态变量等变量都是共享的,包含 PHP 的全局缓冲区。
所以在开发时特地须要留神作用域相干的问题。
协程的 I / O 连贯
在协程中,要特地留神不能共用一个 I/O 连贯,否则会造成数据异样。
因为协程的穿插运行机制,且各个协程的 I/O 连贯都必须是互相独立的,这时如果应用传统的间接建设连贯形式,会导致每个协程都须要建设连贯、闭关连贯,从而耗费大量资源。那么该如何解决协程的 I/O 连贯问题呢?这个时候就须要用到连接池了。
连接池存在的意义在于,复用原来的连贯,从而节俭反复建设连贯所带来的开销。
协程的理论利用场景
说了这么多,那协程倒底能解决哪些理论业务场景呢?上面通过一个实例来疾速上手协程(笔者过后写这篇文章时,对协程的了解还不够粗浅,所以这里援用 zxr615 的”做饭“的例子来了解协程):
传统同步阻塞实现逻辑:
<?php | |
function cook() | |
{$startTime = time(); | |
echo "开始煲汤..." . PHP_EOL; | |
sleep(10); | |
echo "汤好了..." . PHP_EOL; | |
echo "开始煮饭..." . PHP_EOL; | |
sleep(8); | |
echo "饭熟了..." . PHP_EOL; | |
echo "放油..." . PHP_EOL; | |
sleep(1); | |
echo "煎鱼..." . PHP_EOL; | |
sleep(3); | |
echo "放盐..." . PHP_EOL; | |
sleep(1); | |
echo "出锅..." . PHP_EOL; | |
var_dump('总耗时:' . (time() - $startTime) . '分钟'); | |
} | |
cook(); |
协程实现逻辑:
<?php | |
use Swoole\Coroutine; | |
use Swoole\Coroutine\WaitGroup; | |
use Swoole; | |
class Cook | |
{public function cookByCo() | |
{$startTime = time(); | |
// 开启一键协程化: https://wiki.swoole.com/#/runtime?id=swoole_hook_all | |
Swoole\Runtime::enableCoroutine($flags = SWOOLE_HOOK_ALL); | |
// 创立一个协程容器: https://wiki.swoole.com/#/coroutine/scheduler | |
// 相当于进入厨房 | |
\Co\run(function () { | |
// 期待后果: https://wiki.swoole.com/#/coroutine/wait_group?id=waitgroup | |
// 记录哪道菜做好了,哪道菜还须要多长时间 | |
$wg = new WaitGroup(); | |
// 保留数据的后果 | |
// 装好的菜 | |
$result = []; | |
// 记录一下煲汤(记录一个工作) | |
$wg->add(); | |
// 创立一个煲汤工作(开启一个新的协程) | |
Coroutine::create(function () use ($wg, &$result) { | |
echo "开始煲汤..." . PHP_EOL; | |
// 煲汤须要 6 分钟,所以咱们也不必在这里等汤煮好,// 间接去做下一个工作:炒菜(协程切换) | |
sleep(8); | |
echo "汤好了..." . PHP_EOL; | |
// 装盘 | |
$result['soup'] = '一锅汤'; | |
$wg->done(); // 标记工作实现}); | |
// 记录一下煮饭(记录一个工作) | |
$wg->add(); | |
// 创立一个煮饭工作(开启一个新的协程) | |
Coroutine::create(function () use ($wg, &$result) { | |
echo "开始煮饭..." . PHP_EOL; | |
// 煮饭须要 5 分钟,所以咱们不必在这里等饭煮熟,放在这里一会再来看看好了没有 | |
// 咱们先去煲汤(协程切换) | |
sleep(10); | |
echo "饭熟了..." . PHP_EOL; | |
// 装盘 | |
$result['rice'] = '一锅米饭'; | |
$wg->done(); // 标记工作实现}); | |
// 记录一下炒菜 | |
$wg->add(); | |
// 创立一个炒菜工作(再开启一个新的协程) | |
Coroutine::create(function () use ($wg, &$result) { | |
// 煎鱼的过程必须放在一个协程外面执行,如果不是的话可能鱼还没煎好就出锅了 | |
// 因为开启协程后,IO 全是异步了,在此 demo 中每次遇到 sleep 都会挂起以后协程 | |
// 切换到下一个协程执行。// 例如把出锅这一步开启一个新协程执行,则在煎鱼的时候鱼,鱼就出锅了。echo "放油..." . PHP_EOL; | |
sleep(1); | |
echo "煎鱼..." . PHP_EOL; | |
sleep(3); | |
echo "放盐..." . PHP_EOL; | |
sleep(1); | |
echo "出锅..." . PHP_EOL; | |
// 装盘 | |
$result['food'] = '鱼香肉丝'; | |
$wg->done();}); | |
// 期待全副工作实现 | |
$wg->wait(); | |
// 返回数据(上菜!) | |
var_dump($result); | |
}); | |
var_dump('总耗时:' . (time() - $startTime) . '分钟'); | |
} | |
} | |
$cooker = new Cook(); | |
$cooker->cookByCo(); |
通过执行代码能够看到协程形式比传统阻塞形式足足快了十三分钟。从协程形式实现的逻辑中能够看到,通过无感知编写”同步代码“,却实现了异步 I/O 的成果和性能。防止了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无奈保护。
不过须要留神的是传统回调的触发条件是 回调函数 ,而协程切换的条件是 遇到 I/O。
协程误区
理论应用协程时,须要留神以下几个误区,否则成果可能会事倍功半。
实践上来讲,协程解决的是 I/O 复用的问题,对于计算密集的问题有效。
- 如果 cpu 很闲(大部分工夫都耗费在网络磁盘上了),协程就能够进步 cpu 的利用率
- 如果 cpu 自身就很饱和了 用协程反而会升高 cpu 利用率(须要花工夫来做协程调度)。
- swoole 是单线程
参考链接
- swoole 学习笔记 - 做一顿饭来了解协程
- 协程 -EasySwoole
- swoole 协程 -swoole 高手之路
- swoole 一个协程问题?为什么效率变慢了