検索
ホームページバックエンド開発PHP7PHP7でのコルーチンの実装

PHP7でのコルーチンの実装

May 02, 2020 am 11:08 AM
php7コルーチン

まえがき

誰もが「コルーチン」という概念について聞いたことがあると思います。

しかし、一部の学生はこの概念を理解しておらず、実装方法、使用方法、使用場所がわかりません。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 は、一種のユーザー空間スレッドとして理解できます。

コルーチンにはいくつかの特徴があります。

コラボレーション。プログラマ自身が作成したスケジューリング戦略であるため、プリエンプションではなくコラボレーションを通じて切り替わります。

ユーザーがステート完了を作成するとき、切り替えと破壊

#⚠️ プログラミングの観点から見ると、コルーチンの考え方は本質的に制御フローのアクティブな降伏および再開メカニズムです

#ジェネレーターはコルーチンの実装によく使用されます

ここまで言っても、コルーチンの基本的な概念は理解できたはずですよね?

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 コルーチン

コルーチンの紹介で前述したように、コルーチンではプログラマー自身がスケジューリング メカニズムを記述する必要があります。このメカニズムの記述方法を見てみましょう。

ジェネレーターの正しい使い方

ジェネレーターは関数のように直接呼び出すことができないので、どのように呼び出すことができるのでしょうか。

メソッドは次のとおりです:

foreach his

send($value)

current / next...

タスクの実装

タスクはタスクの抽象化です。先ほどコルーチンはユーザー空間のスレッドであると言いました。スレッドは関数を実行するものとして理解できます。

したがって、Task コンストラクターは、コルーチンと名付けたクロージャー関数を受け取ります。

/**
 * 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;
        }
    }
}

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的执行权
    }
}

这样就提高了程序的执行效率。

关于『系统调用』的实现,鸟哥已经讲得很明白,我这里不再说明。

协程堆栈

鸟哥文中还有一个协程堆栈的例子。

我们上面说过了,如果在函数中使用了yield,就不能当做函数使用。

所以你在一个协程函数中嵌套另外一个协程函数:

<?php
function echoTimes($msg, $max) {
    for ($i = 1; $i <= $max; ++$i) {
        echo "$msg iteration $i\n";
        yield;
    }
}
 
function task() {
    echoTimes(&#39;foo&#39;, 10); // print foo ten times
    echo "---\n";
    echoTimes(&#39;bar&#39;, 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(&#39;bar&#39;, 5);
}

这样就实现了一个协程堆栈,现在你可以举一反三了。

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(&#39;bar&#39;, 5);
}

这样,轻松调用子协程。

建议不要使用PHP的Yield来实现协程,推荐使用swoole,2.0已经支持了协程,并附带了部分案例。

推荐教程:《PHP7教程

以上がPHP7でのコルーチンの実装の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明
この記事はsegmentfaultで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

AI Hentai Generator

AI Hentai Generator

AIヘンタイを無料で生成します。

ホットツール

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

このプロジェクトは osdn.net/projects/mingw に移行中です。引き続きそこでフォローしていただけます。 MinGW: GNU Compiler Collection (GCC) のネイティブ Windows ポートであり、ネイティブ Windows アプリケーションを構築するための自由に配布可能なインポート ライブラリとヘッダー ファイルであり、C99 機能をサポートする MSVC ランタイムの拡張機能が含まれています。すべての MinGW ソフトウェアは 64 ビット Windows プラットフォームで実行できます。

PhpStorm Mac バージョン

PhpStorm Mac バージョン

最新(2018.2.1)のプロフェッショナル向けPHP統合開発ツール

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

SublimeText3 英語版

SublimeText3 英語版

推奨: Win バージョン、コードプロンプトをサポート!

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境