PHP5.5 の優れた新機能の 1 つは、ジェネレーターとコルーチンのサポートです。ジェネレーターについては、PHP のドキュメントや他のさまざまなブログ投稿 (この投稿やこの記事など) ですでに詳しく説明されています。コルーチンは比較的注目されていないため、コルーチンは非常に強力な機能を持っていますが、知るのが難しく、説明するのも困難です。
この記事では、コルーチンを使用してタスク スケジューリングを実装する方法を説明し、例を通じてテクノロジーを理解します。最初の 3 つのセクションで背景を簡単に説明します。すでに十分な基礎ができている場合は、「共同マルチタスク」セクションに直接ジャンプできます。
ジェネレーター
ジェネレーターの最も基本的な考え方も関数です。この関数の戻り値は、単一の値を返すのではなく、順番に出力されます。言い換えれば、ジェネレーターを使用すると、反復子インターフェースの実装が容易になります。以下は xrange 関数を実装して簡単に説明します:
foreach (xrange(1, 1000000) as $num) {
echo $num, "n";
}
上記の xrange() 関数は、PHP の組み込み関数 range() と同じ機能を提供します。ただし、異なる点は、 range() 関数が 1 ~ 100 万のグループ値を含む配列を返すことです (注: マニュアルを確認してください)。 xrange() 関数はこれらの値を順番に出力するイテレータを返しますが、実際には配列の形で計算されるわけではありません。
この方法の利点は明らかです。これにより、大規模なデータのコレクションを一度にメモリにロードせずに処理できます。無限大のデータ ストリームを処理することもできます。
もちろん、この関数はジェネレーターを介さずに、Iteratorインターフェースを継承して実装することもできます。イテレータ インターフェイスで 5 つのメソッドを実装するよりも、ジェネレータを使用して実装する方が便利です。
ジェネレーターは割り込み可能な関数です
ジェネレーターからコルーチンを理解するには、コルーチンが内部でどのように動作するかを理解することが非常に重要です。ジェネレーターは割り込み可能な関数であり、ジェネレーターでは、yield が割り込みポイントを構成します。
上記の例に従って、xrange(1,1000000) を呼び出した場合、xrange() 関数のコードは実際には実行されません。代わりに、PHP はイテレーター インターフェイスを実装するジェネレーター クラスのインスタンスを返すだけです。
コルーチン
コルーチンが上記の機能に追加する主な機能は、データをジェネレーターに送り返す機能です。これにより、生成側から呼び出し側への一方向通信が、両者の間の双方向通信に変わります。 ジェネレーターの next() メソッドの代わりに send() メソッドを呼び出して、データをコルーチンに渡します。次の logger() コルーチンは、この通信がどのように機能するかを示す例です:
コードをコピーします
fwrite($fileHandle, yield . "n");
}
}
$logger = logger(__DIR__ . '/log');
$logger->send('Foo');
ご覧のとおり、ここでは yield はステートメントとしてではなく、式として使用されています。つまり、戻り値があります。 yield の戻り値は、send() メソッドに渡される値です。 この例では、yield は最初に「Foo」を返し、次に「Bar」を返します。
上記の例では、yield はレシーバーとしてのみ機能します。 2 つの使用法、つまり受信と送信の両方を混合することが可能です。通信の送受信の仕組みの例は次のとおりです:
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')); 4) 「ret1」(gen の最初の var_dump)
「ret1」 ;
出力の正確な順序をすぐに理解するのは少し難しいため、なぜそのように出力されるのかを必ず理解してください。特に指摘したい点が 2 つあります。 まず、yield 式の前後に括弧が使用されているのは偶然ではありません。技術的な理由から (Python のように代入の例外を追加することも検討しましたが)、括弧は必要です。次に、current() が呼び出される前に rewind() が呼び出されないことに気づいたかもしれません。これが行われると、巻き戻し操作が暗黙的に実行されたことになります。
上記の logger() の例を読んだ方は、「なぜ双方向通信にコルーチンを使用する必要があるのですか? なぜ共通クラスを使用できないのですか?」と考えますが、その疑問はまったく正しいです。上記の例は基本的な使用法を示していますが、コンテキストではコルーチンを使用する利点が実際には示されていません。コルーチンの例がたくさんあるのはこのためです。上の冒頭で述べたように、コルーチンは非常に強力な概念ですが、そのようなアプリケーションはまれであり、多くの場合非常に複雑です。簡単で実際的な例をいくつか挙げるのは困難です。
この記事では、コルーチンを使用してマルチタスクのコラボレーションを実現することにしました。私たちが解決しようとしている問題は、複数のタスク (または「プログラム」) を同時に実行したいということです。ただし、プロセッサが一度に実行できるタスクは 1 つだけです (この記事の目的はマルチコアを考慮することではありません)。したがって、プロセッサは異なるタスクを切り替えて、常に各タスクを「しばらくの間」実行できるようにする必要があります。
マルチタスク コラボレーションという用語の「コラボレーション」は、この切り替えがどのように実行されるかを説明しています。つまり、現在実行中のタスクが他のタスクを実行できるように、制御をスケジューラーに自動的に戻す必要があります。これは、スケジューラが好むと好まざるにかかわらず、しばらく実行されているタスクを中断できる「プリエンプティブ」マルチタスクとは対照的です。協調マルチタスクは Windows (Windows 95) と Mac OS の初期バージョンで使用されていましたが、後にプリエンプティブ マルチタスクの使用に切り替えられました。理由は非常に明らかです。制御を自動的に戻すプログラムに依存していると、行儀の悪いソフトウェアが CPU 全体を自分自身で占有し、他のタスクと共有しないことが容易になります。この時点で、コルーチンとタスク スケジューリングの間の関係を理解する必要があります。yield 命令は、タスクがそれ自体を中断し、制御をスケジューラに移す方法を提供します。したがって、コルーチンは他の複数のタスクを実行できます。さらに、yield を使用してタスクとスケジューラ間の通信を行うこともできます。 私たちの目的は、「タスク」に対してより軽量なパッケージ化コルーチン関数を使用することです:
コードをコピーします
コードは次のとおりです:
クラスタスク {
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();
}
}
yield 'bar';
}
$gen = gen();
// 最初の yield の前に send() が発生するため、暗黙的な rewind() 呼び出しが行われます。
$gen->rewind();
var_dump($gen-> send('something'));
// rewind() は最初の yield に進み (そしてその値を無視します)、send() は// 2 番目の yield に進みます (そしてその値をダンプします)。したがって、最初に得られた値が失われます!
复制码
クラス スケジューラ {
protected $maxTaskId = 0;
protected $taskMap = []; // タスク ID => task
protected $taskQueue;
パブリック関数 __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;
}
公開関数スケジュール(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->スケジュール($タスク);
}
}
}
}
newTask() メソッド (次の空のタスク ID を使用) は新しいタスクを作成し、次にそのタスクをタスク マッピング グループに配置します。いずれかのタスクが終了した場合、そのタスクはリストの最後に再度調整されるかどうかを確認します。何かあります)任务的调度器:
for ($i = 1; $i <= 10; ++$i) {
echo "これはタスク 1 の反復 $i.n";
yield;
}
}
for ($i = 1; $i <= 5; ++$i) {
echo "これはタスク 2 の反復 $i.n";
yield;
}
}
$scheduler->newTask(task1());
$scheduler->newTask(task2());
出力は実際に期待どおりです。最初の 5 回の反復では、2 つのタスクが交互に実行され、2 番目のタスクが終了した後は、最初のタスクのみが実行され続けます。
スケジューラーとのコミュニケーション
スケジューラーが実行されたので、スケジュールの次の項目、タスクとスケジューラー間の通信に進みましょう。プロセスがオペレーティング システムと通信するために使用するのと同じ方法、つまりシステム コールを使用して通信します。システムコールが必要な理由は、オペレーティングシステムがプロセスとは異なるアクセス許可レベルにあるためです。したがって、特権レベルの操作 (別のプロセスの強制終了など) を実行するには、カーネルがその操作を実行できるように、何らかの方法で制御をカーネルに戻す必要があります。繰り返しますが、この動作は割り込み命令を使用することで内部的に実現されます。以前は一般的な int 命令が使用されていましたが、現在はより具体的で高速な syscall/sysenter 命令が使用されています。
私たちのタスク スケジューリング システムはこの設計を反映しています。単純にスケジューラーをタスクに渡す (つまり、タスクが望むことを何でもできるようにする) のではなく、情報を yield 式に渡すことによってシステム コールと通信します。ここでの Yield は割り込みであり、スケジューラに情報を送信する (およびスケジューラから情報を渡す) 方法です。
システム コールを説明するために、呼び出し可能なシステム コールの小さなカプセル化を作成します:
class SystemCall {
protected $callback;
パブリック関数 __construct(callable $callback) {
$this->callback = $callback;
}
public function __invoke(Task $task, Scheduler $scheduler) {
$callback = $this->callback; // PHP では直接呼び出すことはできません :/
return $callback($task, $scheduler);
}
}
他の呼び出し可能なものと同様に (_invoke を使用して) 実行されますが、スケジューラーが呼び出しタスクとそれ自体をこの関数に渡す必要があります。この問題を解決するには、スケジューラの実行メソッドを少し変更する必要があります:
;
最初のシステムコールはタスク ID を返すだけです:
コードをコピー
コードは次のとおりです:
function getTaskId() {
$scheduler->schedule($task);
});}
function task($max) {
$tid = (yield getTaskId()); // <-- これがシステムコールです!
for ($i = 1; $i <= $max; ++$i) {
echo "これはタスク $tid 反復 $i.n です";
「これはタスク $tid 反復 $i.n です」;
$scheduler = 新しいスケジューラ;
$scheduler->newTask(task(5));
$scheduler->run();
このコードは、前の例と同じ出力を返します。システムコールは他のコールと同様に通常通り実行されますが、事前にyieldが追加されていることに注意してください。新しいタスクを作成して強制終了するには、2 つ以上のシステム コールが必要です:
コードをコピーします
function killTask($tid) {
return new SystemCall(
function(Task $task, Scheduler $scheduler) use ($tid) {
$task->setSendValue($scheduler->killTask($tid));
$スケジューラ - &gt;スケジュール($ task);
killTask 関数はスケジューラにメソッドを追加する必要があります:
コードをコピーします
コードは次のとおりです:
if (!isset($this->taskMap[$tid])) { return false;
}
}
http://www.bkjia.com/PHPjc/328024.html
www.bkjia.com
true
技術記事