PHP でコルーチンを使用して協調マルチタスクを実現する ページ 1/2_PHP チュートリアル

WBOY
リリース: 2016-07-21 15:01:16
オリジナル
867 人が閲覧しました

PHP5.5 の優れた新機能の 1 つは、ジェネレーターとコルーチンのサポートです。ジェネレーターについては、PHP のドキュメントや他のさまざまなブログ投稿 (この投稿やこの記事など) ですでに詳しく説明されています。コルーチンは比較的注目されていないため、コルーチンは非常に強力な機能を持っていますが、知るのが難しく、説明するのも困難です。

この記事では、コルーチンを使用してタスク スケジューリングを実装する方法を説明し、例を通じてテクノロジーを理解します。最初の 3 つのセクションで背景を簡単に説明します。すでに十分な基礎ができている場合は、「共同マルチタスク」セクションに直接ジャンプできます。

ジェネレーター

ジェネレーターの最も基本的な考え方も関数です。この関数の戻り値は、単一の値を返すのではなく、順番に出力されます。言い換えれば、ジェネレーターを使用すると、反復子インターフェースの実装が容易になります。以下は xrange 関数を実装して簡単に説明します:

コードをコピー コードは次のとおりです:

function xrange($start, $end, $step = 1) {
for ($i = $start; $i < = $end ; $i += $step) {
yield $i;
}
}

foreach (xrange(1, 1000000) as $num) {
echo $num, "n";
}

上記の xrange() 関数は、PHP の組み込み関数 range() と同じ機能を提供します。ただし、異なる点は、 range() 関数が 1 ~ 100 万のグループ値を含む配列を返すことです (注: マニュアルを確認してください)。 xrange() 関数はこれらの値を順番に出力するイテレータを返しますが、実際には配列の形で計算されるわけではありません。

この方法の利点は明らかです。これにより、大規模なデータのコレクションを一度にメモリにロードせずに処理できます。無限大のデータ ストリームを処理することもできます。

もちろん、この関数はジェネレーターを介さずに、Iteratorインターフェースを継承して実装することもできます。イテレータ インターフェイスで 5 つのメソッドを実装するよりも、ジェネレータを使用して実装する方が便利です。

ジェネレーターは割り込み可能な関数です
ジェネレーターからコルーチンを理解するには、コルーチンが内部でどのように動作するかを理解することが非常に重要です。ジェネレーターは割り込み可能な関数であり、ジェネレーターでは、yield が割り込みポイントを構成します。

上記の例に従って、xrange(1,1000000) を呼び出した場合、xrange() 関数のコードは実際には実行されません。代わりに、PHP はイテレーター インターフェイスを実装するジェネレーター クラスのインスタンスを返すだけです。

コードをコピー コードは次のとおりです。

$range = xrange(1, 1000000);
var_dump ($range); // オブジェクト(ジェネレーター)#1
var_dump($range インスタンスオブイテレーター) // bool(true)
オブジェクトに対して iterator メソッドを 1 回呼び出すと、その中のコードが 1 回実行されます。たとえば、$range->rewind() を呼び出すと、xrange() 内のコードが制御フローに実行され、そこで初めてyield が発生します。この場合、yield $i は $i=$start の場合にのみ実行されることを意味します。 yield ステートメントに渡される値は、$range->current() を使用して取得されます。
ジェネレーターでコードの実行を続けるには、$range->next() メソッドを呼び出す必要があります。これにより、yield ステートメントが表示されるまでジェネレーターが再度開始されます。したがって、 next() メソッドと current() メソッドを連続して呼び出すと、ある時点で yield ステートメントがなくなるまで、ジェネレーターからすべての値が取得されます。 xrange() の場合、$i が $end を超えるとこの状況が発生します。この場合、制御フローは関数の最後に到達するため、コードは実行されません。これが発生すると、 void() メソッドは false を返し、反復は終了します。



コルーチン

コルーチンが上記の機能に追加する主な機能は、データをジェネレーターに送り返す機能です。これにより、生成側から呼び出し側への一方向通信が、両者の間の双方向通信に変わります。 ジェネレーターの next() メソッドの代わりに send() メソッドを呼び出して、データをコルーチンに渡します。次の logger() コルーチンは、この通信がどのように機能するかを示す例です:


コードをコピーします

コードは次のとおりです:
function logger($fileName) {
$fileHandle = fopen($fileName, 'a');

while (true) {

fwrite($fileHandle, yield . "n");
}
}

$logger = logger(__DIR__ . '/log');
$logger->send('Foo');

$logger->send('Bar')


ご覧のとおり、ここでは 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();
}
}

setSendValue() メソッドを使用して、これらの値を次回の通知に送信するように指定できます (その後、これが必要であることがわかります)。 run() 関数は実際に実行されます。 send() メソッドを使用するプログラムを除いて、FirstYieldflag の前に追加するものとして、次のコードセグメントを考慮する必要があることを理解してください。

复制代码代码如下:

関数 gen() {

yield 'foo';

yield 'bar';
}

$gen = gen();

var_dump($gen->send('something'));


// 最初の yield の前に send() が発生するため、暗黙的な rewind() 呼び出しが行われます。

// したがって、実際に起こることは次のとおりです:

$gen->rewind();
var_dump($gen-> send('something'));

// rewind() は最初の yield に進み (そしてその値を無視します)、send() は// 2 番目の yield に進みます (そしてその値をダンプします)。したがって、最初に得られた値が失われます!



beforeFirstYieldcondition を追加することで、最初の 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 を使用) は新しいタスクを作成し、次にそのタスクをタスク マッピング グループに配置します。いずれかのタスクが終了した場合、そのタスクはリストの最後に再度調整されるかどうかを確認します。何かあります)任务的调度器:

复制代码代码如下:

function task1() {

for ($i = 1; $i <= 10; ++$i) {
echo "これはタスク 1 の反復 $i.n";
yield;
}
}

function task2() {

for ($i = 1; $i <= 5; ++$i) {
echo "これはタスク 2 の反復 $i.n";
yield;
}
}

$scheduler = 新しいスケジューラ;

$scheduler->newTask(task1());

$scheduler->newTask(task2());

$scheduler->run();


2 つのタスクは両方とも 1 つの情報だけを返し、その後、yield 制御を使用してレギュレータに送信します。

复制代码代码如下:
これはタスク 1 の反復 1 です。
これはタスク 2 の反復 1 です。
これはタスク 1 の反復 2 です。
これはタスク 2 の反復 2 です。
これはタスク 1 です反復 3.
これはタスク 2 の反復 3.
これはタスク 1 の反復 4.
これはタスク 2 の反復 4.
これはタスク 1 の反復 5.
これはタスク 2 の反復 5.
これはタスク 1 の反復 6 .
これはタスク 1 の反復です 7.
これはタスク 1 の反復です 8.
これはタスク 1 の反復です 9.
これはタスク 1 の反復です 10.

出力は実際に期待どおりです。最初の 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 を使用して) 実行されますが、スケジューラーが呼び出しタスクとそれ自体をこの関数に渡す必要があります。この問題を解決するには、スケジューラの実行メソッドを少し変更する必要があります:

コードをコピーします コードは次のとおりです:

public function run() {
while (!$this->taskQueue->isEmpty()) {
$task = $this- >taskQueue->dequeue();
$retval = $task->run(); if($ retval

;
最初のシステムコールはタスク ID を返すだけです:


コードをコピー

コードは次のとおりです:


function getTaskId() {

return new SystemCall(function(Task $task, Scheduler $scheduler) {

$task->setSendValue ($task ->getTaskId());

$scheduler->schedule($task);

});

}

この関数は、タスク ID を次回送信される値に設定し、タスクを再度スケジュールします。システムコールが使用されているため、スケジューラは自動的にタスクを呼び出すことができず、タスクを手動でスケジュールする必要があります (理由は後ほど説明します)。この新しいシステムコールを使用するには、前の例を書き直す必要があります:


コードをコピーします

コードは次のとおりです:

function task($max) {
$tid = (yield getTaskId()); // <-- これがシステムコールです!
for ($i = 1; $i <= $max; ++$i) {
echo "これはタスク $tid 反復 $i.n です";
「これはタスク $tid 反復 $i.n です」;
$scheduler = 新しいスケジューラ;

$scheduler->newTask(task(10));

$scheduler->newTask(task(5));

$scheduler->run();


このコードは、前の例と同じ出力を返します。システムコールは他のコールと同様に通常通り実行されますが、事前にyieldが追加されていることに注意してください。新しいタスクを作成して強制終了するには、2 つ以上のシステム コールが必要です:


コードをコピーします

コードは次のとおりです:
function newTask(Generator $coroutine) {
「」""""""" ;

$scheduler->schedule($task);


function killTask​​($tid) {
return new SystemCall(
function(Task $task, Scheduler $scheduler) use ($tid) {
$task->setSendValue($scheduler->killTask​​($tid));
$スケジューラ - &gt;スケジュール($ task);

killTask​​ 関数はスケジューラにメソッドを追加する必要があります:




コードをコピーします

コードは次のとおりです:


パブリック関数 killTask​​($tid) {

if (!isset($this->taskMap[$tid])) { return false;

}

unset($this->taskMap[$tid]); // これは少し醜いので、キューをたどる必要がないように最適化できる可能性があります。 // しかし、タスクを強制終了することはかなりまれであると仮定して、今は気にしません
foreach ($this->taskQueue AS $ i = & gt; $ task) {
IF ($ task-& gt; gettaskid () === $ TID) {
unset ($ this- & gt; taskqueue [$ i]); return true;

}




http://www.bkjia.com/PHPjc/328024.html

www.bkjia.com

tru​​e

http://www.bkjia.com/PHPjc/328024.html


技術記事

PHP5.5 の優れた新機能の 1 つは、ジェネレーターとコルーチンのサポートです。ジェネレーター、PHP、その他さまざまなブログ投稿 (これやこの記事など) に関するドキュメントがすでにあります...


このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート