Swoole4协程专题总结

swoole4出来了也好段时间了,很久之前也用swoole协程做过一些东西。最近由于公司在项目中想用swoole优化并行处理一些逻辑业务,于是重新看了协swoole4,发现这次版本对协程的支持更加完善了,于是有了整理此出此文章的想法,目的让大家比较清晰了解到swoole协程的一些例子或者使用,特别是没用过或者没接触过协程的朋友。想起当年自己了解协程是啥的时候,也是四处找不到什么好文章。在我看来,这样系统的总结起来,才是最快入坑的。

什么是协程

大家应该都知道线程是CPU的最小调度单元,而协程可以认为是用户状态下的线程,所以是依赖进程存在的。但是协程不能利用多核CPU,不能跨进程调度,但它的调度不需要系统参与,所以创建销毁和切换的成本很低,所以在swoole的文档也有写到,在协程中,1s内你可以读写1万次文件,睡眠1万次,用PDO和mysqli与数据库通信1万次等

协程环境(协程容器)

在swoole的协程中,所有的协程必须在协程环境或者说协程容器内创建,才会发生协程调度。在swoole4中,大概有以下三种情况是创建协程容器环境的:

  1. 异步风格的服务端程序start方法.详见官方文档异步服务端程序章节
  2. Process或者Proce\Pool的start方法。详见官方文档进程管理章节
  3. 自主创建自定义协程容器:Co\run()方法。使用例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Co\run(function () {
    go(function () {
    Co::sleep(3);
    echo "c[1]\n";
    });
    go(function () {
    echo "c[2]\n";
    });
    });
    echo "outside";
    /*程序输出结果:
    c[2]
    c[1]
    outside*/
    其实Co::run方法是对Swoole\Coroutine\Scheduler类的封装,了解更多,可见官方文档协程容器章节。关于scheduler的使用例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$scheduler = new Swoole\Coroutine\Scheduler();
//和go函数不同,add方法添加的协程,要等start调用后才一起并发执行,不调用不执行,其中a,b是传进来等参数
$scheduler->add(function ($a, $b) {
Co::sleep(2);
echo $a . PHP_EOL;
echo $b . PHP_EOL;
}, "add1", 12345);
$scheduler->add(function ($a, $b) {
Co::sleep(1);
echo $a . PHP_EOL;
echo $b . PHP_EOL;
}, "add2", 12345);

//在 start 时会同时启动 $num 个 $fn 协程,并行地执行。
$scheduler->parallel(10, function ($t) {
Co:
sleep($t);
echo "Co" . Co::getCid() . PHP_EOL;
}, 0.05);
$scheduler->start();
/*运行结果:
Co3
Co4
Co5
Co6
Co7
Co8
Co9
Co10
Co11
Co12
add2
12345
add1
12345*/

协程调度

  • 手动调度:yield让出执行权,然后resume这种方式。例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    Co\run(function () {

    $cid = go(function () {
    echo "co[1] start\n";
    co::yield();//让出当前执行权,不基于io调度。且必须和resume成对使用,否则会发生协程泄漏
    echo "co[1] end\n";
    });

    go(function () use ($cid) {
    echo "co[2] start\n";
    co::resume($cid);//恢复某个协程的执行权
    echo "co[2] end\n";
    });
    });
    /*运行结果:
    co[1] start
    co[2] start
    co[1] end
    co[2] end*/
  • 基于IO调度:发生IO阻塞,自动让出执行权,等IO准备好再次自动恢复执行权。下面的一键协程化,就是把原有PHP的一些同步IO,使得在协程容器中进行的是协程方式的异步IO调度。

    一键协程化(同步IO变协程异步IO)

众所周知,在PHP中很多请求外部资源的操作都是同步IO的,如curl一个http请求,用pdo连接查询数据库,或者用file_get_contents()读写文件等等,在发生IO请求时,往往同步的代码会在请求第三方服务的时候,发生阻塞等待。为了让这些场景实现异步IO,swoole4底层扩展是采用了PHP HOOK的方式,hook掉大部署产生IO阻塞的function,让其在底层实现协程方式的IO调度。例如你想要在curl中发生协程调度,只要在协程环境前调用一行代码即可:

1
Co::set(['hook_flags' => SWOOLE_HOOK_CURL])

应用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Co::set(['hook_flags' => SWOOLE_HOOK_CURL]);
Co\run(function () {
go(function () {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://www.baidu.com/");
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo "curl finisned" . PHP_EOL;
//var_dump($result);
});
echo "here" . PHP_EOL;
});

/*运行结果:
here
curl finisned*/

/*注视掉//Co::set(['hook_flags' => SWOOLE_HOOK_CURL]);,由于curl不会再发生协程调用,所有运行结果:
curl finisned
here
*/

了解更多能Hook的函数,参见章节一键协程化

协程API

swoole4封装好的一些协程API,包括常用的:Swoole\Coroutine::getCid(),Swoole\Coroutine::yield()等,这里说一个有趣的api方法,就是Swoole\Coroutine::defer()方法,此方法作用是在协程关闭前调用,用于资源的释放,就算抛异常也会执行。例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Co\run(function () {
go(function () {
co::sleep(1.0);
echo "co[1] end\n";
});
go(function () {
Swoole\Coroutine::defer(function () {
echo "defer" . PHP_EOL;
});//协程关闭前调用,就行放在所有的逻辑前面,也总是在协程关闭前调用
echo "co[2] end\n";
//throw new Exception("co[2] exception");
});
});
/*
运行结果:
co[2] end
defer
co[1] end
*/

更多API参见官方文档协程API章节

协程进阶

  • channel
    channel类似于php的数组,仅仅使用内存,没其他资源申请,无IO消耗,而且是基于引用计数实现的,零内存拷贝,传递巨大字符或者数组都不会产生额外性能消耗。
    1. channel主要用来协程间通信的,不能跨进程使用。
    2. channel重要特性是,当channel push满了,会阻塞当前协程,直到有其他协程pop出后恢复到当前程序执行处。相反,当channel pop空了,也会阻塞当前协程,直到有其他协程push数据进来才恢复执行。这种特性很像生产者和消费者之间的调度。
  • 连接池:结和channel的pop和push之间的交互阻塞调用的特性,可以用channel来存新建的连接(也就是连接的存放池),每来一个请求pop出一个连接,当channel无连接可以pop时,会阻塞等待其他请求释放连接到channel中(也就是连接池中)。当然pop()方法是有超时参数设置的,超过等待时间没其他请求释放连接push进来,当前协程不再阻塞等待。具体详细实战,可参考本人很久之前分享的文章swoole自行打造mysql连接池,当时官方对于连接池的文档是非常少,而且swoole没现成的封装,现在看到官方已经出了连接池的封装,可以直接参考使用连接池章节
  • 并发调用:也利用channel的pop和push产生阻塞调用的特性,申请长度为N的channel,然后循环创建N个子协程去执行逻辑,每个子协程执行完毕后push数据进channel,然后主协程循环pop出来。因为初次channel为空,就达到了主协程阻塞等待子协程请求数据返回push进channel然后再恢复执行主协程的效果。官方已经针对这个特性专门封装了并发调用类WaitGroup。实战使用见本人另一文章实战协程并发和多进程任务处理或者了解并发调用的可参考官方文档WaitGroup章节

结束语

至此,关于swoole4协程相关的一些东西基本总结完毕,可能没说到什么原理性的东西,这些有待大家日后一起学习探讨,主要还是应用和例子为主。感觉这次swoole的4版本已经升级支持协程的很多东西了,比起之前的版本,用起来实在爽了好多。

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2015-2020 谭家俊
  • Powered by Hexo Theme Ayer
  • PV: UV:

请我喝杯咖啡吧~

微信