# 2.3 協程原理
## 協程
### 基本概念
“協程”(Coroutine)概念最早由 Melvin Conway 于1958年提出。協程可以理解為純用戶態的線程,其通過協作而不是搶占來進行切換。相對于進程或者線程,協程所有的操作都可以在用戶態完成,創建和切換的消耗更低。總的來說,協程為協同任務提供了一種運行時抽象,這種抽象非常適合于協同多任務調度和數據流處理。在現代操作系統和編程語言中,因為用戶態線程切換代價比內核態線程小,協程成為了一種輕量級的多任務模型。
從編程角度上看,協程的思想本質上就是控制流的主動讓出(yield)和恢復(resume)機制,迭代器常被用來實現協程,所以大部分的語言實現的協程中都有yield關鍵字,比如Python、PHP、Lua。但也有特殊比如Go就使用的是通道來通信。
### 協程與進程線程的區別
* 對于操作系統來說只有進程和線程,協程的控制由應用程序顯式調度,非搶占式的
* 協程的執行最終靠的還是線程,應用程序來調度協程選擇合適的線程來獲取執行權
* 切換非常快,成本低。一般占用棧大小遠小于線程(協程KB級別,線程MB級別),所以可以開更多的協程
* 協程比線程更輕量級
### PHP與協程
PHP從5.5引入了yield關鍵字,增加了迭代生成器和協程的支持,但并未在語言本身級別實現一個完善的協程解決方案。PHP協程也是基于Generator,Generator可以視為一種“可中斷”的函數,而 yield 構成了一系列的“中斷點”。PHP 協程沒有resume關鍵字,而是“在使用的時候喚起”協程。了解如何在PHP中實現協程,首先要解決迭代生成器。
```php
function xrange($start, $end, $step = 1) {
for ($i = $start; $i <= $end; $i += $step) {
yield $i;
}
}
foreach (xrange(1, 1000000) as $num) { // xrange返回的是一個Generator對象
echo $num, "\n";
}
```
具體參考
[PHP > 手冊 > 語言參考 > 生成器](http://php.net/manual/zh/language.generators.overview.php)
### 中斷點
我們從生成器認識協程,需要認識到:生成器是一種具有中斷點的函數,而yield構成了中斷點。比如, 你調用$range->rewind(),那么xrange()里的代碼就會運行到控制流第一次出現yield的地方,而函數內傳遞給yield語句的值,即為迭代的當前值,可以通過$xrange->current()獲取。
### PHP中的協程實現
PHP的協程支持是在迭代生成器的基礎上,增加了可以回送數據給生成器的功能,從而達到雙向通信即:
生成器<---數據--->調用者
#### yield接收與發送數據
```php
function gen() {
$ret = (yield 'yield1');
var_dump($ret);
$ret = (yield 'yield2');
var_dump($ret);
}
$gen = gen();
var_dump($gen->current()); // string(6) "yield1"
var_dump($gen->send('ret1')); // string(4) "ret1" (the first var_dump in gen)
// string(6) "yield2" (the var_dump of the ->send() return value)
var_dump($gen->send('ret2')); // string(4) "ret2" (again from within gen)
// NULL (the return value of ->send())
```
```php
function gen() {
yield 'foo';
yield 'bar';
}
$gen = gen();
var_dump($gen->send('something'));
// 在send之前當$gen迭代器被創建的時候一個renwind()方法已經被隱式調用
// 所以實際上發生的應該類似:
//$gen->rewind();
//var_dump($gen->send('something'));
//這樣renwind的執行將會導致第一個yield被執行, 并且忽略了他的返回值.
//真正當我們調用yield的時候, 我們得到的是第二個yield的值! 導致第一個yield的值被忽略.
//string(3) "bar"
```
#### 協程與任務調度
yield指令提供了任務中斷自身的一種方法,然后把控制交回給任務調度器。而PHP語言本身只是提供程序中斷的功能,至于任務調度器需要我們自己實現,同時協程在運行多個其他任務時,yield還可以用來在任務和調度器之間進行通信。
#### PHP協程任務
簡單的定義具有任何ID標識的協程函數,如一個輕量級的協程函數示例代碼:
```php
<?php
class Task {
protected $taskId;
protected $coroutine;
protected $sendValue = null;
protected $beforeFirstYield = true;
public function __construct($taskId, Generator $coroutine) {
$this->taskId = $taskId;
$this->coroutine = $coroutine;
}
public function getTaskId() {
return $this->taskId;
}
public function setSendValue($sendValue) {
$this->sendValue = $sendValue;
}
public function run() {
if ($this->beforeFirstYield) {
$this->beforeFirstYield = false;
return $this->coroutine->current();
} else {
$retval = $this->coroutine->send($this->sendValue);
$this->sendValue = null;
return $retval;
}
}
public function isFinished() {
return !$this->coroutine->valid();
}
}
```
#### PHP協程調度器
簡單來說,是可以在多個任務之間相互協調,及任務之間相互切換的一種進程資源的分配器。調度器的實現方式有多種,大致分為兩類:一是,隊列;二是,定時器。
```php
class Scheduler {
protected $maxTaskId = 0;
protected $taskMap = []; // taskId => task
protected $taskQueue;
public function __construct() {
$this->taskQueue = new SplQueue();
}
public function newTask(Generator $coroutine) {
$tid = ++$this->maxTaskId;
$task = new Task($tid, $coroutine);
$this->taskMap[$tid] = $task;
$this->schedule($task);
return $tid;
}
public function schedule(Task $task) {
$this->taskQueue->enqueue($task);
}
public function run() {
while (!$this->taskQueue->isEmpty()) {
$task = $this->taskQueue->dequeue();
$task->run();
if ($task->isFinished()) {
unset($this->taskMap[$task->getTaskId()]);
} else {
$this->schedule($task);
}
}
}
}
```
newTask()方法創建一個新任務,然后把這個任務放入任務map數組里,接著它通過把任務放入任務隊列里來實現對任務的調度。接著run()方法掃描任務隊列,運行任務,如果一個任務結束了,那么它將從隊列里刪除,否則它將在隊列的末尾再次被調度。
協程示例:
```php
function task1() {
for ($i = 1; $i <= 10; ++$i) {
echo "This is task 1 iteration $i.\n";
yield;
}
}
function task2() {
for ($i = 1; $i <= 5; ++$i) {
echo "This is task 2 iteration $i.\n";
yield;
}
}
$scheduler = new Scheduler;
$scheduler->newTask(task1());
$scheduler->newTask(task2());
$scheduler->run();
```
結果如下
```
This is task 1 iteration 1.
This is task 2 iteration 1.
This is task 1 iteration 2.
This is task 2 iteration 2.
This is task 1 iteration 3.
This is task 2 iteration 3.
This is task 1 iteration 4.
This is task 2 iteration 4.
This is task 1 iteration 5.
This is task 2 iteration 5.
This is task 1 iteration 6.
This is task 1 iteration 7.
This is task 1 iteration 8.
This is task 1 iteration 9.
This is task 1 iteration 10.
```
- 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 附錄