PHP的多任務協程處理的分析

來源:互聯網
上載者:User
這篇文章主要介紹了關於PHP的多任務協程處理,有著一定的參考價值,現在分享給大家,有需要的朋友可以參考一下

那麼,開始吧!

這就是本文我們要討論的問題。不過我們會從更簡單更熟悉的樣本開始。

一切從數組開始

我們可以通過簡單的遍曆來使用數組:

$array = ["foo", "bar", "baz"]; foreach ($array as $key => $value) {    print "item: " . $key . "|" . $value . "\n";} for ($i = 0; $i < count($array); $i++) {    print "item: " . $i . "|" . $array[$i] . "\n";}

這是我們日常編碼所依賴的基本實現。可以通過遍曆數組擷取每個元素的鍵名和索引值。

當然,如果我們希望能夠知道在何時可以使用數組。PHP 提供了一個方便的內建函數:

print is_array($array) ? "yes" : "no"; // yes

類數組處理

有時,我們需要對一些資料使用相同的方式進行遍曆處理,但它們並非數群組類型。比如對 DOMDocument 類進行處理:

$document = new DOMDocument();$document->loadXML("<p></p>");$elements = $document->getElementsByTagName("p");print_r($elements); // DOMNodeList Object ( [length] => 1 )

這顯然不是一個數組,但是它有一個 length 屬性。我們能像遍曆數組一樣,對其進行遍曆嗎?我們可以判斷它是否實現了下面這個特殊的介面:

print ($elements instanceof Traversable) ? "yes" : "no"; // yes

這真的太有用了。它不會導致我們在遍曆非可遍曆資料時觸發錯誤。我們僅需在處理前進行檢測即可。

不過,這會引發另外一個問題:我們能否讓自訂類也擁有這個功能呢?回答是肯定的!第一個實現方法類似如下:

class MyTraversable implements Traversable{    //  在這裡編碼...}

如果我們執行這個類,我們將看到一個錯誤資訊:

PHP Fatal error: Class MyTraversable must implement interface Traversable as part of either Iterator or IteratorAggregate

Iterator(迭代器)

我們無法直接實現 Traversable,但是我們可以嘗試第二種方案:

class MyTraversable implements Iterator{    //  在這裡編碼...}

這個介面需要我們實現 5 個方法。讓我們完善我們的迭代器:

class MyTraversable implements Iterator{    protected $data;    protected $index = 0;    public function __construct($data)    {        $this->data = $data;    }    public function current()    {        return $this->data[$this->index];    }    public function next()    {        return $this->data[$this->index++];    }    public function key()    {        return $this->index;    }    public function rewind()    {        $this->index = 0;    }    public function valid()    {        return $this->index < count($this->data);    }}

這邊我們需要注意幾個事項:

  1. 我們需要儲存構造器方法傳入的 $data 數組,以便後續我們可以從中擷取它的元素。

  2. 還需要一個內部索引(或指標)來跟蹤 currentnext 元素。

  3. rewind() 僅僅重設 index 屬性,這樣 current()next() 才能正常工作。

  4. 鍵名並非只能是數字類型!這裡使用數組索引是為了保證樣本足夠簡單。

我們可以向下面這樣運行這段代碼:

$iterator = new MyIterator(["foo", "bar", "baz"]); foreach ($iterator as $key => $value) {    print "item: " . $key . "|" . $value . "\n";}

這看起來需要處理太多工作,但是這是能夠像數組一樣使用 foreach/for 功能的一個簡潔實現。

IteratorAggregate(彙總迭代器)

還記得第二個介面拋出的 Traversable 異常嗎?下面看一個比實現 Iterator 介面更快的實現吧:

class MyIteratorAggregate implements IteratorAggregate{    protected $data;    public function __construct($data)    {        $this->data = $data;    }    public function getIterator()    {        return new ArrayIterator($this->data);    }}

這裡我們作弊了。相比於實現一個完整的 Iterator,我們通過 ArrayIterator() 裝飾。不過,這相比於通過實現完整的 Iterator 簡化了不少代碼。

兄弟莫急!先讓我們比較一些代碼。首先,我們在不使用產生器的情況下從檔案中讀取每一行資料:

$content = file_get_contents(__FILE__);$lines = explode("\n", $content);foreach ($lines as $i => $line) {    print $i . ". " . $line . "\n";}

這段代碼讀取檔案自身,然後會列印出每行的行號和代碼。那麼為什麼我們不使用產生器呢!

function lines($file) {    $handle = fopen($file, 'r');    while (!feof($handle)) {        yield trim(fgets($handle));    }    fclose($handle);}foreach (lines(__FILE__) as $i => $line) {    print $i . ". " . $line . "\n";}

我知道這看起來更加複雜。不錯,不過這是因為我們沒有使用 file_get_contents() 函數。一個產生器看起來就像是一個函數,但是它會在每次擷取到 yield 關鍵詞是停止運行。

產生器看起來有點像迭代器:

print_r(lines(__FILE__)); // Generator Object ( )

儘管它不是迭代器,它是一個 Generator。它的內部定義了什麼方法呢?

print_r(get_class_methods(lines(__FILE__))); // Array// (//     [0] => rewind//     [1] => valid//     [2] => current//     [3] => key//     [4] => next//     [5] => send//     [6] => throw//     [7] => __wakeup// )
如果你讀取一個大檔案,然後使用 memory_get_peak_usage(),你會注意到產生器的代碼會使用固定的記憶體,無論這個檔案有多大。它每次進度去一行。而是用 file_get_contents() 函數讀取整個檔案,會使用更大的記憶體。這就是在迭代處理這類事物時,產生器的能給我們帶來的優勢!

Send(發送資料)

可以將資料發送到產生器中。看下下面這個產生器:

<?php$generator = call_user_func(function() {    yield "foo";});print $generator->current() . "\n"; // foo
注意這裡我們如何在 call_user_func() 函數中封裝產生器函數的?這裡僅僅是一個簡單的函數定義,然後立即調用它擷取一個新的產生器執行個體...

我們已經見過 yield 的用法。我們可以通過擴充這個產生器來接收資料:

$generator = call_user_func(function() {    $input = (yield "foo");    print "inside: " . $input . "\n";});print $generator->current() . "\n";$generator->send("bar");

資料通過 yield 關鍵字傳入和返回。首先,執行 current() 代碼直到遇到 yield,返回 foosend() 將輸出傳入到產生器列印輸入的位置。你需要習慣這種用法。

拋出異常(Throw)

由於我們需要同這些函數進行互動,可能希望將異常推送到產生器中。這樣這些函數就可以自行處理異常。

看看下面這個樣本:

$multiply = function($x, $y) {    yield $x * $y;};print $multiply(5, 6)->current(); // 30

現在讓我們將它封裝到另一個函數中:

$calculate = function ($op, $x, $y) use ($multiply) {    if ($op === 'multiply') {        $generator = $multiply($x, $y);        return $generator->current();    }};print $calculate("multiply", 5, 6); // 30

這裡我們通過一個普通閉包將乘法產生器封裝起來。現在讓我們驗證無效參數:

$calculate = function ($op, $x, $y) use ($multiply) {    if ($op === "multiply") {        $generator = $multiply($x, $y);        if (!is_numeric($x) || !is_numeric($y)) {            throw new InvalidArgumentException();        }        return $generator->current();    }};print $calculate('multiply', 5, 'foo'); // PHP Fatal error...

如果我們希望能夠通過產生器處理異常?我們怎樣才能將異常傳入產生器呢!

$multiply = function ($x, $y) {    try {        yield $x * $y;    } catch (InvalidArgumentException $exception) {        print "ERRORS!";    }};$calculate = function ($op, $x, $y) use ($multiply) {    if ($op === "multiply") {        $generator = $multiply($x, $y);        if (!is_numeric($x) || !is_numeric($y)) {            $generator->throw(new InvalidArgumentException());        }        return $generator->current();    }};print $calculate('multiply', 5, 'foo'); // PHP Fatal error...

棒呆了!我們不僅可以像迭代器一樣使用產生器。還可以通過它們發送資料並拋出異常。它們是可中斷和可恢複的函數。有些語言把這些函數叫做……

我們可以使用協程(coroutines)來構建非同步代碼。讓我們來建立一個簡單的任務發送器。首先我們需要一個 Task 類:

class Task{    protected $generator;    public function __construct(Generator $generator)    {        $this->generator = $generator;    }    public function run()    {        $this->generator->next();    }    public function finished()    {        return !$this->generator->valid();    }}

Task 是普通產生器的裝飾器。我們將產生器賦值給它的成員變數以供後續使用,然後實現一個簡單的 run()finished() 方法。run() 方法用於執行任務,finished() 方法用於讓發送器知道何時終止運行。

然後我們需要一個 Scheduler 類:

class Scheduler{    protected $queue;    public function __construct()    {        $this->queue = new SplQueue();    }    public function enqueue(Task $task)    {        $this->queue->enqueue($task);    }    pulic function run()    {        while (!$this->queue->isEmpty()) {            $task = $this->queue->dequeue();            $task->run();            if (!$task->finished()) {                $this->queue->enqueue($task);            }        }    }}

Scheduler 用於維護一個待執行的任務隊列。run() 會彈出隊列中的所有任務並執行它,直到運行完整個隊列任務。如果某個任務沒有執行完畢,當這個任務本次運行完成後,我們將再次入列。

SplQueue 對於這個樣本來講再合適不過了。它是一種 FIFO(先進先出:fist in first out) 資料結構,能夠確保每個任務都能夠擷取足夠的處理時間。

我們可以像這樣運行這段代碼:

$scheduler = new Scheduler();$task1 = new Task(call_user_func(function() {    for ($i = 0; $i < 3; $i++) {        print "task1: " . $i . "\n";        yield;    }}));$task2 = new Task(call_user_func(function() {    for ($i = 0; $i < 6; $i++) {        print "task2: " . $i . "\n";        yield;    }}));$scheduler->enqueue($task1);$scheduler->enqueue($task2);$scheduler->run();

運行時,我們將看到如下執行結果:

task 1: 0task 1: 1task 2: 0task 2: 1task 1: 2task 2: 2task 2: 3task 2: 4task 2: 5

這幾乎就是我們想要的執行結果。不過有個問題發生在首次運行每個任務時,它們都執行了兩次。我們可以對 Task 類稍作修改來修複這個問題:

class Task{    protected $generator;    protected $run = false;    public function __construct(Generator $generator)    {        $this->generator = $generator;    }    public function run()    {        if ($this->run) {            $this->generator->next();        } else {            $this->generator->current();        }        $this->run = true;    }    public function finished()    {        return !$this->generator->valid();    }}

我們需要調整首次 run() 方法調用,從產生器當前有效指標讀取運行。後續調用可以從下一個指標讀取運行...

有些人基於這個思路實現了一些超贊的類庫。我們來看看其中的兩個...

RecoilPHP

RecoilPHP 是一套基於協程的類庫,它最令人印象深刻的是用於 ReactPHP 核心。可以將事件迴圈在 RecoilPHP 和 RecoilPHP 之間進行交換,而你的程式無需架構上的調整。

我們來看一下 ReactPHP 非同步 DNS 解決方案:

function resolve($domain, $resolver) {    $resolver        ->resolve($domain)        ->then(function ($ip) use ($domain) {            print "domain: " . $domain . "\n";            print "ip: " . $ip . "\n";        }, function ($error) {                        print $error . "\n";        })}function run(){    $loop = React\EventLoop\Factory::create();     $factory = new React\Dns\Resolver\Factory();     $resolver = $factory->create("8.8.8.8", $loop);     resolve("silverstripe.org", $resolver);    resolve("wordpress.org", $resolver);    resolve("wardrobecms.com", $resolver);    resolve("pagekit.com", $resolver);     $loop->run();} run();

resolve() 接收網域名稱和 DNS 解析器,並使用 ReactPHP 執行標準的 DNS 尋找。不用太過糾結與 resolve() 函數內部。重要的是這個函數不是產生器,而是一個函數!

run() 建立一個 ReactPHP 事件迴圈,DNS 解析器(這裡是個工廠執行個體)解析若干網域名稱。同樣,這個也不是一個產生器。

想知道 RecoilPHP 到底有何不同?還希望掌握更多細節!

use Recoil\Recoil; function resolve($domain, $resolver){    try {        $ip = (yield $resolver->resolve($domain));         print "domain: " . $domain . "\n";        print "ip: " . $ip . "\n";    } catch (Exception $exception) {        print $exception->getMessage() . "\n";    }} function run(){    $loop = (yield Recoil::eventLoop());     $factory = new React\Dns\Resolver\Factory();     $resolver = $factory->create("8.8.8.8", $loop);     yield [        resolve("silverstripe.org", $resolver),        resolve("wordpress.org", $resolver),        resolve("wardrobecms.com", $resolver),        resolve("pagekit.com", $resolver),    ];} Recoil::run("run");

通過將它整合到 ReactPHP 來完成一些令人稱奇的工作。每次運行 resolve() 時,RecoilPHP 會管理由 $resoler->resolve() 返回的 promise 對象,然後將資料發送給產生器。此時我們就像在編寫同步代碼一樣。與我們在其他一步模型中使用回調代碼不同,這裡只有一個指令列表。

RecoilPHP 知道它應該管理一個有執行 run() 函數時返回的 yield 數組。RoceilPHP 還支援基於協程的資料庫(PDO)和日誌庫。

IcicleIO

IcicleIO 為了一全新的方案實現 ReactPHP 一樣的目標,而僅僅使用協程功能。相比 ReactPHP 它僅包含極少的組件。但是,核心的非同步流、伺服器、Socket、事件迴圈特性一個不落。

讓我們看一個 socket 伺服器樣本:

use Icicle\Coroutine\Coroutine;use Icicle\Loop\Loop;use Icicle\Socket\Client\ClientInterface;use Icicle\Socket\Server\ServerInterface;use Icicle\Socket\Server\ServerFactory; $factory = new ServerFactory(); $coroutine = Coroutine::call(function (ServerInterface $server) {    $clients = new SplObjectStorage();         $handler = Coroutine::async(        function (ClientInterface $client) use (&$clients) {            $clients->attach($client);                         $host = $client->getRemoteAddress();            $port = $client->getRemotePort();                         $name = $host . ":" . $port;                         try {                foreach ($clients as $stream) {                    if ($client !== $stream) {                        $stream->write($name . "connected.\n");                    }                }                 yield $client->write("Welcome " . $name . "!\n");                                 while ($client->isReadable()) {                    $data = trim(yield $client->read());                                         if ("/exit" === $data) {                        yield $client->end("Goodbye!\n");                    } else {                        $message = $name . ":" . $data . "\n";                                                foreach ($clients as $stream) {                            if ($client !== $stream) {                                $stream->write($message);                            }                        }                    }                }            } catch (Exception $exception) {                $client->close($exception);            } finally {                $clients->detach($client);                foreach ($clients as $stream) {                    $stream->write($name . "disconnected.\n");                }            }        }    );         while ($server->isOpen()) {        $handler(yield $server->accept());    }}, $factory->create("127.0.0.1", 6000)); Loop::run();

據我所知,這段代碼所做的事情如下:

  1. 在 127.0.0.1 和 6000 連接埠建立一個伺服器執行個體,然後將其傳入外部產生器.

  2. 外部產生器運行,同時伺服器等待新串連。當伺服器接收一個串連它將其傳入內部產生器。

  3. 內部產生器寫入訊息到 socket。當 socket 可讀時運行。

  4. 每次 socket 向伺服器發送訊息時,內部產生器檢測訊息是否是退出標識。如果是,通知其他 socket。否則,其它 socket 發送這個相同的訊息。

開啟命令列終端輸入 nc localhost 6000 查看執行結果!

該樣本使用 SplObjectStorage 跟蹤 socket 串連。這樣我們就可以向所有 socket 發送訊息。

這個話題可以包含很多內容。希望您能看到產生器是如何建立的,以及它們如何協助編寫迭代程式和非同步代碼。

如果你有問題,可以隨時問我。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.