序文
誰もが「コルーチン」という概念を聞いたことがあると思います。
しかし、一部の学生はこの概念を理解しておらず、実装方法、使用方法、使用場所がわかりません。yield がコルーチンであるとさえ考えている人もいます。
私は常に、知識ポイントを正確に表現できない場合は、単に理解していないだけだと考えています。
これまでに PHP を使用してコルーチンを実装することについて学んだことがある場合は、Niao 兄弟の記事を読んでいるはずです:Using coroutines toimple multi-task scheduling in PHP | Fengxuezhiyu
Brother Niao の記事は以下から翻訳されました。外国人著者の訳文は簡潔明瞭で、具体的な例も挙げられています。
この記事を書く目的は、ニアオ兄弟の記事をより完全に補足することですが、結局のところ、一部の生徒は基礎が十分ではなく、混乱しています。
コルーチンとは何ですか?
まず、コルーチンとは何なのかを理解してください。
「プロセス」と「スレッド」という概念を聞いたことがあるかもしれません。
プロセスは、.exe ファイルがクラスであり、プロセスが新しいインスタンスであるのと同様に、コンピューター メモリ内で実行されているバイナリ実行可能ファイルのインスタンスです。
プロセスは、コンピュータ システムにおけるリソース割り当てとスケジューリングの基本単位です (スケジューリング単位のスレッド プロセスについては心配しないでください)。各 CPU は同時に 1 つのプロセスのみを処理できます。
いわゆる同時実行性は、CPU が複数のことを同時に処理できるように見えるだけですが、シングルコア CPU の場合、実際には異なるプロセス間を非常に高速に切り替えています。
プロセスの切り替えにはシステムコールが必要で、CPU は現在のプロセスに関するさまざまな情報を保存する必要があり、CPUCache も破棄されます。
したがって、プロセスを切り替えることができない場合は、必要がない限り切り替えないでください。
では、「プロセスを切り替えられない場合は、必要がない限り切り替えない」を実現するにはどうすればよいでしょうか?
まず、プロセスが切り替わる条件は、プロセスの実行が完了する、プロセスに割り当てられたCPUタイムスライスが終了する、処理が必要なシステムで割り込みが発生する、またはプロセスが切り替わる条件です。プロセスが必要なリソースを待っている(プロセスのブロック)など。よく考えたら先の状況は言うことないんですが、ブロックして待ってるのは無駄じゃないでしょうか?
実際、ブロックされても、プログラムを実行するための実行可能な場所は他にあるため、愚かに待つ必要はありません。
したがって、スレッドがあります。
スレッドを簡単に理解すると、特に関数 (論理フロー) を実行する「マイクロプロセス」です。
したがって、スレッドを使用して、プログラムの作成プロセス中に同時に実行できる関数を具体化できます。
スレッドには 2 種類あり、1 つはカーネルによって管理およびスケジュールされます。
カーネルが管理とスケジューリングに参加する必要がある限り、コストは非常に高くなります。この種のスレッドは、プロセス内で実行中のスレッドが障害に遭遇したときに、別の実行可能なスレッドの実行をスケジュールできますが、そのスレッドは依然として同じプロセス内にあるため、プロセスの切り替えが行われないという問題を実際に解決します。
別の種類のスレッドがあり、そのスケジューリングはプログラムを作成するプログラマー自身によって管理され、カーネルには見えません。この種のスレッドは「ユーザー空間スレッド」と呼ばれます。
Coroutine は、一種のユーザー空間スレッドとして理解できます。
コルーチンにはいくつかの特徴があります:
- コラボレーション。プログラマ自身が作成したスケジューリング戦略であるため、プリエンプションではなくコラボレーションを通じて切り替えます
- 完全な作成、ユーザー モードでの切り替えと破棄
- ⚠️ プログラミングの観点から見ると、コルーチンのアイデアは本質的に制御フロー メカニズムのアクティブな降伏と再開です
- generator はコルーチンの実装によく使用されます
ここまで言っても、コルーチンの基本的な概念は理解できたはずです。
PHPによるコルーチンの実装
概念の説明からステップバイステップで解説します!
反復可能なオブジェクト
PHP5 では、foreach
ステートメントを使用するなど、セルのリストを横断できるオブジェクトを定義する方法が提供されています。
反復可能なオブジェクトを実装したい場合は、Iterator
インターフェイスを実装する必要があります:
<?php class MyIterator implements Iterator { private $var = array(); public function __construct($array) { if (is_array($array)) { $this->var = $array; } } public function rewind() { echo "rewinding\n"; reset($this->var); } public function current() { $var = current($this->var); echo "current: $var\n"; return $var; } public function key() { $var = key($this->var); echo "key: $var\n"; return $var; } public function next() { $var = next($this->var); echo "next: $var\n"; return $var; } public function valid() { $var = $this->current() !== false; echo "valid: {$var}\n"; return $var; } } $values = array(1,2,3); $it = new MyIterator($values); foreach ($it as $a => $b) { print "$a: $b\n"; }
Generator
これは、順番に次のように言えます。反復可能なオブジェクトを持つには、foreach
によってトラバースされるオブジェクトについては、一連のメソッドを実装する必要があります。yield
キーワードは、このプロセスを簡素化するためのものです。
ジェネレーターは、単純なオブジェクトの反復を実装する簡単な方法を提供します。Iterator
インターフェイスを実装するクラスを定義する場合と比較して、パフォーマンスのオーバーヘッドと複雑さが大幅に軽減されます。
<?php function xrange($start, $end, $step = 1) { for ($i = $start; $i <= $end; $i += $step) { yield $i; } } foreach (xrange(1, 1000000) as $num) { echo $num, "\n"; }
覚えておいてください、yield
が関数で使用されている場合、それはジェネレーターです。直接呼び出しても意味がありません。関数のように実行することはできません。
つまり、yield
は yield
です。次回、yield
がコルーチンであると言う人がいたら、私は間違いなくあなたを xxxx として扱います。
PHP コルーチン
コルーチンの紹介で前述したように、コルーチンではプログラマー自身がスケジューリング メカニズムを記述する必要があります。このメカニズムの記述方法を見てみましょう。
0)生成器正确使用
既然生成器不能像函数一样直接调用,那么怎么才能调用呢?
方法如下:
- foreach他
- send($value)
- current / next...
1)Task实现
Task就是一个任务的抽象,刚刚我们说了协程就是用户空间线程,线程可以理解就是跑一个函数。
所以Task的构造函数中就是接收一个闭包函数,我们命名为coroutine
。
/** * Task任务类 */ class Task { protected $taskId; protected $coroutine; protected $beforeFirstYield = true; protected $sendValue; /** * Task constructor. * @param $taskId * @param Generator $coroutine */ public function __construct($taskId, Generator $coroutine) { $this->taskId = $taskId; $this->coroutine = $coroutine; } /** * 获取当前的Task的ID * * @return mixed */ public function getTaskId() { return $this->taskId; } /** * 判断Task执行完毕了没有 * * @return bool */ public function isFinished() { return !$this->coroutine->valid(); } /** * 设置下次要传给协程的值,比如 $id = (yield $xxxx),这个值就给了$id了 * * @param $value */ public function setSendValue($value) { $this->sendValue = $value; } /** * 运行任务 * * @return mixed */ public function run() { // 这里要注意,生成器的开始会reset,所以第一个值要用current获取 if ($this->beforeFirstYield) { $this->beforeFirstYield = false; return $this->coroutine->current(); } else { // 我们说过了,用send去调用一个生成器 $retval = $this->coroutine->send($this->sendValue); $this->sendValue = null; return $retval; } } }
2)Scheduler实现
接下来就是Scheduler
这个重点核心部分,他扮演着调度员的角色。
/** * Class Scheduler */ Class Scheduler { /** * @var SplQueue */ protected $taskQueue; /** * @var int */ protected $tid = 0; /** * Scheduler constructor. */ public function __construct() { /* 原理就是维护了一个队列, * 前面说过,从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制 * */ $this->taskQueue = new SplQueue(); } /** * 增加一个任务 * * @param Generator $task * @return int */ public function addTask(Generator $task) { $tid = $this->tid; $task = new Task($tid, $task); $this->taskQueue->enqueue($task); $this->tid++; return $tid; } /** * 把任务进入队列 * * @param Task $task */ public function schedule(Task $task) { $this->taskQueue->enqueue($task); } /** * 运行调度器 */ public function run() { while (!$this->taskQueue->isEmpty()) { // 任务出队 $task = $this->taskQueue->dequeue(); $res = $task->run(); // 运行任务直到 yield if (!$task->isFinished()) { $this->schedule($task); // 任务如果还没完全执行完毕,入队等下次执行 } } } }
这样我们基本就实现了一个协程调度器。
你可以使用下面的代码来测试:
<?php function task1() { for ($i = 1; $i <= 10; ++$i) { echo "This is task 1 iteration $i.\n"; yield; // 主动让出CPU的执行权 } } function task2() { for ($i = 1; $i <= 5; ++$i) { echo "This is task 2 iteration $i.\n"; yield; // 主动让出CPU的执行权 } } $scheduler = new Scheduler; // 实例化一个调度器 $scheduler->addTask(task1()); // 添加不同的闭包函数作为任务 $scheduler->addTask(task2()); $scheduler->run();
关键说下在哪里能用得到PHP协程。
function task1() { /* 这里有一个远程任务,需要耗时10s,可能是一个远程机器抓取分析远程网址的任务,我们只要提交最后去远程机器拿结果就行了 */ remote_task_commit(); // 这时候请求发出后,我们不要在这里等,主动让出CPU的执行权给task2运行,他不依赖这个结果 yield; yield (remote_task_receive()); ... } function task2() { for ($i = 1; $i <= 5; ++$i) { echo "This is task 2 iteration $i.\n"; yield; // 主动让出CPU的执行权 } }
这样就提高了程序的执行效率。
关于『系统调用』的实现,鸟哥已经讲得很明白,我这里不再说明。
3)协程堆栈
鸟哥文中还有一个协程堆栈的例子。
我们上面说过了,如果在函数中使用了yield
,就不能当做函数使用。
所以你在一个协程函数中嵌套另外一个协程函数:
<?php function echoTimes($msg, $max) { for ($i = 1; $i <= $max; ++$i) { echo "$msg iteration $i\n"; yield; } } function task() { echoTimes('foo', 10); // print foo ten times echo "---\n"; echoTimes('bar', 5); // print bar five times yield; // force it to be a coroutine } $scheduler = new Scheduler; $scheduler->addTask(task()); $scheduler->run();
这里的echoTimes是执行不了的!所以就需要协程堆栈。
不过没关系,我们改一改我们刚刚的代码。
把Task中的初始化方法改下,因为我们在运行一个Task的时候,我们要分析出他包含了哪些子协程,然后将子协程用一个堆栈保存。(C语言学的好的同学自然能理解这里,不理解的同学我建议去了解下进程的内存模型是怎么处理函数调用)
/** * Task constructor. * @param $taskId * @param Generator $coroutine */ public function __construct($taskId, Generator $coroutine) { $this->taskId = $taskId; // $this->coroutine = $coroutine; // 换成这个,实际Task->run的就是stackedCoroutine这个函数,不是$coroutine保存的闭包函数了 $this->coroutine = stackedCoroutine($coroutine); }
当Task->run()的时候,一个循环来分析:
/** * @param Generator $gen */ function stackedCoroutine(Generator $gen) { $stack = new SplStack; // 不断遍历这个传进来的生成器 for (; ;) { // $gen可以理解为指向当前运行的协程闭包函数(生成器) $value = $gen->current(); // 获取中断点,也就是yield出来的值 if ($value instanceof Generator) { // 如果是也是一个生成器,这就是子协程了,把当前运行的协程入栈保存 $stack->push($gen); $gen = $value; // 把子协程函数给gen,继续执行,注意接下来就是执行子协程的流程了 continue; } // 我们对子协程返回的结果做了封装,下面讲 $isReturnValue = $value instanceof CoroutineReturnValue; // 子协程返回`$value`需要主协程帮忙处理 if (!$gen->valid() || $isReturnValue) { if ($stack->isEmpty()) { return; } // 如果是gen已经执行完毕,或者遇到子协程需要返回值给主协程去处理 $gen = $stack->pop(); //出栈,得到之前入栈保存的主协程 $gen->send($isReturnValue ? $value->getValue() : NULL); // 调用主协程处理子协程的输出值 continue; } $gen->send(yield $gen->key() => $value); // 继续执行子协程 } }
然后我们增加echoTime的结束标示:
class CoroutineReturnValue { protected $value; public function __construct($value) { $this->value = $value; } // 获取能把子协程的输出值给主协程,作为主协程的send参数 public function getValue() { return $this->value; } } function retval($value) { return new CoroutineReturnValue($value); }
然后修改echoTimes
:
function echoTimes($msg, $max) { for ($i = 1; $i <= $max; ++$i) { echo "$msg iteration $i\n"; yield; } yield retval(""); // 增加这个作为结束标示 }
Task
变为:
function task1() { yield echoTimes('bar', 5); }
这样就实现了一个协程堆栈,现在你可以举一反三了。
4)PHP7中yield from关键字
PHP7中增加了yield from
,所以我们不需要自己实现携程堆栈,真是太好了。
把Task的构造函数改回去:
public function __construct($taskId, Generator $coroutine) { $this->taskId = $taskId; $this->coroutine = $coroutine; // $this->coroutine = stackedCoroutine($coroutine); //不需要自己实现了,改回之前的 }
echoTimes
函数:
function echoTimes($msg, $max) { for ($i = 1; $i <= $max; ++$i) { echo "$msg iteration $i\n"; yield; } }
task1
生成器:
function task1() { yield from echoTimes('bar', 5); }
这样,轻松调用子协程。
总结
这下应该明白怎么实现PHP协程了吧?
建议不要使用PHP的Yield来实现协程,推荐使用swoole,2.0已经支持了协程,并附带了部分案例。
End...