在协程相关的内容中,最后我们要讲的就是这个一键协程化的功能。这玩意又是什么意思呢?我们先看下面的例子。
go(function(){
$i = 9999999999999;
while($i--){
// file_put_contents("./4.7test", $i.PHP_EOL);
}
echo 111;
});
go(function(){
echo 222;
});
先不考虑循环中间那个写文件的操作,现在这两个协程,你觉得会是怎么运行的?注意,外面有没有 run 都是一样的。
如果你试了就会发现,while() 会阻塞协程。又来了,又是同步阻塞和异步非阻塞的问题,来来回回有完没有完。
这还真没办法,【Swoole系列4.5】协程并发调度https://mp.weixin.qq.com/s/41hKQqTkHNolBxHJMM4vAg 中,我们讲过 sleep() 在容器内外的表现会有不同,但 while() 则是不管有没有容器都会阻塞的。同样,如果是一个非常耗时的 IO 任务,也会带来这样的问题,这就会让本来可以异步来回切换调度的协程功能退化成一个同步阻塞的功能。这可不是我们期望的结果呀。
好在 Swoole 团队的各路大神们经过不断地努力研究,总算克服了这个问题。最开始,他们也是一个一个地写异步客户端,但是呢?实现复杂,很难完美支持;用户需要更改的代码很多;很难覆盖所有操作。于是,它们转换了实现思路,通过 Hook 原生的 PHP 函数实现的方式来实现协程客户端。也就是给原生的那些功能函数加外挂,让它们直接就可以支持协程化操作。
具体表现是什么呢?就是没什么表现了。我们之前怎么写代码,现在还是怎么写,不用考虑异步阻塞的问题,只需要加上一行代码就行了。这下你知道我们上回说过为什么不推荐使用之前的协程 MySQL 和 Redis 客户端了吧,直接使用一键协程化就行了。
\Swoole\Runtime::enableCoroutine();
现在你再试试上面的例子。
\Swoole\Runtime::enableCoroutine();
go(function(){
$i = 9999999999999;
while($i--){
file_put_contents("./4.7test", $i.PHP_EOL);
}
echo 111;
});
go(function(){
echo 222;
});
相信 222 就会先输出出来了。注意,这里要打开中间写文件的注释。为什么呢?因为它所 Hook 的功能函数中,不包含 while() ,我们需要借助里面的一些可以一键协程化的函数来让协程实现调度。也就是说,在文件写入的时候,协程会 yield() 起来,等待后续完成后回来继续执行这个协程中的内容。
具体我们再来看下面官网给出的例子。
Co::set(['hook_flags' => SWOOLE_HOOK_TCP]);
Co\run(function() {
for ($c = 100; $c--;) {
go(function () {//创建100个协程
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);//此处产生协程调度,cpu切到下一个协程,不会阻塞进程
$redis->get('key');//此处产生协程调度,cpu切到下一个协程,不会阻塞进程
});
}
});
Co::set(['hook_flags' => SWOOLE_HOOK_TCP]) 这种形式也可以设置协程化,它和 \Swoole\Runtime::enableCoroutine() 的效果一样,但是它更像是 ini_set() ,也就是可以在程序执行过程中动态地调整。而 \Swoole\Runtime::enableCoroutine() 则最好是在服务启动后马上就进行全局配置,它也支持参数,默认就是 SWOOLE_HOOK_ALL 。参数内容我们后面再说。
在这个测试代码中,创建 100 个协程,每个协程里面都去连接 redis 。注释已经标明得很清楚了,它会在 connect() 的时候埋下勾子 Hook ,然后产生协程调度,也就是 yield() 和 resume() 连接完成后会再切换回来。同样地,get() 的时候也会这样。这就是用户态多协程处理的最典型表现。
不难联想到,PDO 相关的操作也是这样的套路。
对于 Swoole 来说,目前能够 Hook 的内容包括:
redis 扩展
使用 mysqlnd 模式的 pdo_mysql、mysqli 扩展,如果未启用 mysqlnd 将不支持协程化
soap 扩展
file_get_contents、fopen
stream_socket_client (predis、php-amqplib)
stream_socket_server
stream_select (需要 4.3.2 以上版本)
fsockopen
proc_open (需要 4.4.0 以上版本)
curl
无法 Hook 的包括:
mysql:底层使用 libmysqlclient
mongo:底层使用 mongo-c-client
pdo_pgsql
pdo_ori
pdo_odbc
pdo_firebird
php-amqp
说实话,大部分情况下是能够覆盖我们的业务场景了。如果有确实无法支持的,那么还是得靠我们自己去调度实现了。
上面可支持的扩展列表,其实也对应着一系列的参数。比如说 SWOOLE_HOOK_ALL 就是打开全部的协程化支持。另外还可以单独打开下面这些,也可以通过或操作打开某几个。
SWOOLE_HOOK_TCP
SWOOLE_HOOK_UNIX
SWOOLE_HOOK_UDP
SWOOLE_HOOK_UDG
SWOOLE_HOOK_SSL
SWOOLE_HOOK_TLS
SWOOLE_HOOK_SLEEP
SWOOLE_HOOK_FILE
SWOOLE_HOOK_STREAM_FUNCTION
SWOOLE_HOOK_BLOCKING_FUNCTION
SWOOLE_HOOK_PROC
SWOOLE_HOOK_CURL
SWOOLE_HOOK_NATIVE_CURL
SWOOLE_HOOK_SOCKETS
SWOOLE_HOOK_STDIO
每个标签对应的内容大家可以自己去官网上查阅,在这里我也就不做搬运工了。
不容易不容易啊,各位,恭喜下自己吧,我们的协程篇也结束了。不知道大家有什么收获,反正我是收获满满。之前接触过 Java 中的线程,但对协程确实只是有那么一点点概念,没有正式的好好学习过。经过这次,不敢说完全了解,但多少还是让我对协程有了一个新的认识。
至此,Swoole 基础和核心进阶相关的内容全部完成了,其实我们可以看到,在 Swoole 的开发,要转变很多之间的开发思想,同时也要清楚地知道进程和协程相关的内容,这些,其实也就是本系列最核心的内容了。
接下来,我们将继续学习是将是 Swoole 的一些扩展功能和知识点,以及最后的框架方面的学习,精彩内容依然还是值得期待的,大家要坚持下去哦!
测试代码:
https://github.com/zhangyue0503/swoole/blob/main/4.Swoole%E5%8D%8F%E7%A8%8B/source/4.8%E4%B8%80%E9%94%AE%E5%8D%8F%E7%A8%8B%E5%8C%96.php
参考文档:
https://wiki.swoole.com/#/runtime?id=sethookflags