5.1 协程
最后更新于:2022-03-31 23:47:37
# 5.1 协程
在前面的章节[2.3](../chapter-2/2.3-协程原理.md)介绍了协程的原理及PHP用户空间如何实现协程,本节将重点介绍MSF框架的协程如何使用,同时会剖析PHP工程级协程调度器的实现及调度算法。
## 为什么要使用协程?
至于为什么使用协程,可能大家看法还不统一,这里举例来说明,我们实现一个接口,此接口内包含:
2次http请求其他接口A、B,
1次Redis请求C
他们之间的依赖关系为:`((A && B) || C)`
```php
<?php
/**
* 协程示例控制器
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use PG\MSF\Client\Http\Client;
class CoroutineTest extends Controller
{
/**
* 异步回调的方式实现(A && B) || C
*/
public function actionCallBackMode()
{
$client = new \swoole_redis;
$client->connect('127.0.0.1', 6379, function (\swoole_redis $client, $result) {
$client->get('apiCacheForABCallBack', function (\swoole_redis $client, $result) {
if (!$result) {
swoole_async_dns_lookup("www.baidu.com", function($host, $ip) use ($client) {
$cli = new \swoole_http_client($ip, 443, true);
$cli->setHeaders([
'Host' => $host,
]);
$apiA = "";
$cli->get('/', function ($cli) use ($client, $apiA) {
$apiA = $cli->body;
swoole_async_dns_lookup("www.qiniu.com", function($host, $ip) use ($client, $apiA) {
$cli = new \swoole_http_client($ip, 443, true);
$cli->setHeaders([
'Host' => $host,
]);
$apiB = "";
$cli->get('/', function ($cli) use ($client, $apiA, $apiB) {
$apiB = $cli->body;
if ($apiA && $apiB) {
$client->set('apiCacheForABCallBack', $apiA . $apiB, function (\swoole_redis $client, $result) {});
$this->outputJson($apiA . $apiB);
} else {
$this->outputJson('', 'error');
}
});
});
});
});
} else {
$this->outputJson($result);
}
});
});
}
/**
* 协程的方式实现(A && B) || C
*/
public function actionCoroutineMode()
{
// 从Redis获取get apiCacheForABCoroutine
$response = yield $this->getRedisPool('tw')->get('apiCacheForABCoroutine');
if (!$response) {
// 从远程拉取数据
$request = [
'https://www.baidu.com/',
'https://www.qiniu.com/',
];
/**
* @var Client $client
*/
$client = $this->getObject(Client::class);
$results = yield $client->goConcurrent($request);
// 写入redis
$this->getRedisPool('tw')->set('apiCacheForABCoroutine', $results[0]['body'] . $results[1]['body'])->break();
$response = $results[0]['body'] . $results[1]['body'];
}
// 响应结果
$this->outputJson($response);
}
}
```
示例代码:
[https://github.com/pinguo/php-msf-demo/app/Controllers/CoroutineTest.php](https://github.com/pinguo/php-msf-demo/blob/master/app/Controllers/CoroutineTest.php)
http://127.0.0.1:8000/CoroutineTest/CoroutineMode
http://127.0.0.1:8000/CoroutineTest/CallBackMode
1. Swoole实现了异步非阻塞的IO模型它是高性能的基础,但是书写逻辑代码非常复杂,需要多层嵌套回调,阅读和维护困难
2. 基于Yield的协程可以用同步的代码编写方式,达到异步IO的效果和性能,避免了传统异步回调所带来多层回调而导致代码无法维护
## 协程的调度顺序
```php
<?php
/**
* 协程示例控制器
*
* @author camera360_server@camera360.com
* @copyright Chengdu pinguo Technology Co.,Ltd.
*/
namespace App\Controllers;
use PG\MSF\Controllers\Controller;
use PG\MSF\Client\Http\Client;
class CoroutineTest extends Controller
{
// 略...
// 姿势一
public function actionNested()
{
$result = [];
/**
* @var Client $client1
*/
$client1 = $this->getObject(Client::class, ['http://www.baidu.com/']);
yield $client1->goDnsLookup();
/**
* @var Client $client2
*/
$client2 = $this->getObject(Client::class, ['http://www.qq.com/']);
yield $client2->goDnsLookup();
$result[] = yield $client1->goGet('/');
$result[] = yield $client2->goGet('/');
$this->outputJson([strlen($result[0]['body']), strlen($result[1]['body'])]);
}
// 姿势二
public function actionUnNested()
{
$result = [];
/**
* @var Client $client1
*/
$client1 = $this->getObject(Client::class, ['http://www.baidu.com/']);
$dns[] = $client1->goDnsLookup();
/**
* @var Client $client2
*/
$client2 = $this->getObject(Client::class, ['http://www.qq.com/']);
$dns[] = $client2->goDnsLookup();
yield $dns[0];
yield $dns[1];
$req[] = $client1->goGet('/');
$req[] = $client2->goGet('/');
$result[] = yield $req[0];
$result[] = yield $req[1];
$this->outputJson([strlen($result[0]['body']), strlen($result[1]['body'])]);
}
}
```
### 解析
两种姿势看上去都使用了协程的yield关键字;姿势一由于协程在调度时,第一个yield没有接收数据时,程序控制流就不会往下继续执行,从而退化为串行请求第三方接口;姿势二由于DNS查询是异步的,就同时进行多个DNS查询,通过yield关键获取协程执行的结果,再同时异步请求多个接口,最后通过yield关键字获取接口响应结果。
通过两种姿势的对比,使用php-msf协程yield关键字,很好的解决了异步IO回调的写法,让程序看上去是同步执行的,yield起来了接收数据的作用,这也是前面所说的yield具有双向通信的最要特性。
姿势一协程调度过程
send http dns1 lookup->rev http dns1 lookup->send http dns2 lookup->rev http dns2 lookup->send get1->rev get1->send get2-> rev get2
姿势二协程调度过程
send http dns1 lookup->send http dns2 lookup->rev http dns1 lookup->rev http dns2 lookup->send get1->send get2->rev get1->rev get2
**需要特别注意的是: 如果某个异步IO操作不需要获取返回值,如设置缓存set key val,框架允许不加yield关键字,需要调用break()方法,这可以大大的提升接口的并发能力**
## 使用MSF协程
通过上述的示例代码,我们不难得出MSF协程的使用方式,通常情况下,我们使用异步客户端发送请求,使用yield关键字获取协程任务的运行结果。
### Http
```php
<?php
function Http()
{
// 获取Http客户端
$client = $this->getObject(\PG\MSF\Client\Http\Client::class);
// 单个接口GET请求(自动完成DNS查询->Http请求发送->获取结果)
$res = yield $client->goSingleGet('http://www.baidu.com/');
$client = $this->getObject(\PG\MSF\Client\Http\Client::class);
// 单个接口POST请求(自动完成DNS查询->Http请求发送->获取结果)
$res = yield $client->goSinglePost('http://www.baidu.com/');
// 多个接口同时请求(自动完成DNS查询->Http请求发送->获取结果)
$client = $this->getObject(\PG\MSF\Client\Http\Client::class);
$res = yield $client->goConcurrent(['http://www.baidu.com/', 'http://www.qq.com']);
$client = $this->getObject(\PG\MSF\Client\Http\Client::class);
// 手工DNS查询
yield $client->goDnsLookup('http://www.baidu.com/');
// 手工发送HTTP请求
$res = yield $client->goGet('/');
}
```
### Task
```php
<?php
function Task() {
$idAllloc = $this->getObject(Idallloc::class);
$newId = yield $idAllloc->getNextId();
}
```
### Redis
```php
<?php
function RedisCoroutine() {
$sendRedisGet = $this->getRedisPool('tw')->get('apiCacheForABCoroutine');
$cache = yield $sendRedisGet;
}
```
## MSF协程调度器
MSF协程基于Generator/Yield,IO事件触发,是自主研发的调度器,它的核心思想是发生任何异步IO操作之后,程序的控制流就切换到其他请求,待异步IO可读之后,由程序自行调用调度器接口,进行一次调度,并响应请求。MSF协程完全抛弃了定时器每隔一定时间轮询任务的方案,使得调度器的性能更加接近原生的异步回调方式。
### 关键技术点
* Generator/Yield
* SplStack
* taskMap
### 主要特性
* 协程独立堆栈
* 支持嵌套
* 全调用链路异常捕获
* 调度统计
* 多入口调度
### 协程调度流程
![协程执行流程图](../images/协程执行流程图V2.png "协程执行流程图")