# 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
### 主要特性
* 協程獨立堆棧
* 支持嵌套
* 全調用鏈路異常捕獲
* 調度統計
* 多入口調度
### 協程調度流程

- 0 文檔說明
- 1 為什么研發新框架
- 1.1 傳統php-fpm工作模式的問題
- 1.2 壓測數據對比
- 1.3 小結
- 2 微服務框架研發概覽
- 2.1 通信框架技術選型
- 2.2 swoole
- 2.3 協程原理
- 2.4 異步、并發
- 2.5 小結
- 3 框架運行環境
- 3.1 環境變量
- 3.2 運行代碼
- 3.3 docker
- 3.4 小結
- 4 框架結構
- 4.1 結構概述
- 4.2 控制器
- 4.3 模型
- 4.4 視圖
- 4.5 同步任務
- 4.6 配置
- 4.7 路由
- 4.8 小結
- 5 框架組件
- 5.1 協程
- 5.2 類的加載
- 5.3 異步Http Client
- 5.4 請求上下文
- 5.5 連接池
- 5.6 對象池
- 5.7 RPC
- 5.8 公共庫
- 5.9 RESTful
- 5.10 多語言
- 5.11 雜項
- 5.12 小結
- 6 常見問題
- 7 附錄