想試試,用純PHP
代碼,不依賴第三方拓展就實現"多線程"麼。像 Java
那樣使用 setPriority()
影響各個"線程"的被調用概率,使用join()
等待其餘線程結束;在sleep
期間讓出CPU
佔用,到點再回到該"線程";像 Golang
同樣,用channel
在協程
之間通訊~php
接上回書,講完了 yield 基本用法,這篇文章,帶你們來實戰一下,目標:手把手教會你用 yield 作一個任務調度器,加深對 PHP 生成器 理解。html
建議你們先去看看 以前那篇文章複習下 yield 基礎用法。
好,話很少說,開淦~java
在上一講中,咱們學會了將 function() {...yield...}
就能將一個 函數 變爲 「生成器」git
這就是一個簡單的任務調度器。代碼比較少,直接貼這裏了。github
gitee地址: ./simpleYieldScheduler.phpshell
<?php /** * Class YieldScheduler */ Class YieldScheduler { /** * @var array $gens */ public $gens = array(); /** * 新增任務到 調度器 * * @param Generator $gen * @param null $key * * @return $this */ public function add($gen, $key = null) { if (null === $key) { $this->gens[] = $gen; } else { $this->gens[$key] = $gen; } return $this; } /** * 開始 */ public function start() { $keepRun = true; /** * @var Generator $gen */ $gen = null; do { // 循環調度任務 foreach ($this->gens as $id => $gen) { $re = $gen->current(); echo 'generator id: ' . $id . ' run, get current re : ' . $re . PHP_EOL; $gen->next(); } // 檢查任務是否已完成 foreach ($this->gens as $id => $gen) { $check = $gen->valid(); if (!$check) { // 已執行完畢的任務就能夠踢出任務調度隊列了 unset($this->gens[$id]); } } // 調度器是否完成全部任務 if (0 >= count($this->gens)) { $keepRun = false; } } while ($keepRun); } } function yieldFunc($max = 10) { for($i = 0; $i < $max; $i ++) { (yield $i); } return $i; } $gen1 = yieldFunc(3); $gen2 = yieldFunc(5); $scheduler = new YieldScheduler(); $scheduler->add($gen1)->add($gen2); $scheduler->start();
運行結果:bootstrap
能夠看到咱們用同一個方法和不一樣的入參,生成了兩個不一樣的生成器,用另外一個方法也生成了一個生成器,雖然生成方式不一樣,但不影響他們仨一併啓動,交替運行,他們的執行順序肯定(這個腳本運行多少遍都是同一個結果)。segmentfault
咱們來把這個理解透徹,看到yieldFunc($max)
函數,他寫了一個循環,循環內帶有一個 yield,每當程序運行到這裏時,就會跳出當前函數,讓出運行時。數組
建立好三個 生成器後,再生成一個 YieldScheduler
對象,把兩個 生成器 加入其中,開始運行任務。多線程
在 start()
函數內,就是不斷的逐個調用 current
,next
方法,驅使 生成器
運行,每次運行後,會調用 valid
檢查 生成器
運行完成與否,完成後,就會從 任務調度器
生成器隊列
中踢出該任務。
我這把代碼執行順序僞代碼貼一下:
<?php // do 任務調度器 $sum = 0; $re = $gen1->current(); // 進入 gen1 $n = 0; yield $n++; // 跳出 gen1, 獲取返回值 賦值給 $re echo 'generator id: ' . $id . ' run, get current re : ' . $re . PHP_EOL; $gen1->send($sum++) // sum = 1 // 進入 gen1 $receive = yield; echo 'get scheduler sent : ' . $receive . PHP_EOL; $n++; // 跳出 gen1 // 任務調度器檢查任務是否完成 if (!$gen1->valid()) { unset($gen1); } if (empty($gens)) { break; } // 任務調度器進入第二個循環 // 開始調度 第二個 生成器 $re = $gen2->current(); // 進入 gen2 , $i = 0; if ($i < $max) { yield $i; } // 跳出 gen2 echo 'generator id: ' . $id . ' run, get current re : ' . $re . PHP_EOL; $gen2->send($sum++) // sum = 2 // 進入 gen2 $get = yield; echo 'get scheduler sent : ' . $get . PHP_EOL; $i++; if ($i < $max){ return $i; } // 跳出 gen2 // 任務調度器檢查任務是否完成 if (!$gen2->valid()) { unset($gen2); } if (empty($gens)) { break; } // 任務調度器進入第三個循環 // 開始調度 第三個 生成器 $re = $gen3->current(); // 進入 gen3, 這是第三個生成器,此 $i 不是 gen2 的 $i,因此 $i 從 0開始 $i = 0; if ($i < $max) { yield $i; } // 跳出 gen3 echo 'generator id: ' . $id . ' run, get current re : ' . $re . PHP_EOL; $gen2->send($sum++) // sum = 3 // 進入 gen3 $get = yield; echo 'get scheduler sent : ' . $get . PHP_EOL; $i++; if ($i < $max){ return $i; } // 跳出 gen3 // 任務調度器檢查任務是否完成 if (!$gen3->valid()) { unset($gen3); } if (empty($gens)) { break; } // 任務調度器進入第四個循環 // 又開始調度 第1個 生成器 $re = $gen1->current(); // 進入 gen1 yield $n; // $n = 1, 這裏 $n++ 在第一次調度時,已完成? // 跳出 gen1, 獲取返回值 賦值給 $re echo 'generator id: ' . $id . ' run, get current re : ' . $re . PHP_EOL; $gen1->send($sum++) // sum = 4 // 進入 gen1 $receive = yield; echo 'get scheduler sent : ' . $receive . PHP_EOL; $n++; // 跳出 gen1 // 任務調度器檢查任務是否完成 if (!$gen1->valid()) { unset($gen1); } if (empty($gens)) { break; }
看這僞代碼的執行順序,你想到了什麼呢? goto !, PHP 也支持 goto 語法的,爲了代碼的閱讀,易於維護,通常不多用它。
代碼執行到 yiel
d的右側就跳出,這裏有個細節必定要扣一下,那就是 yield
右側表達式,或者函數執行完,纔會跳出當前 生成器(並非制定到 yield
這一行代碼時,退出)。這個細節,你能夠從 yieldFunc
和 myPrint
調用後的,命令行輸出能夠看到。在 任務調度器
第4個循環調度時,調用 send()
方法後,生成器
內不只執行完畢了 echo 'get scheduler sent : ' . $receive . PHP_EOL;
, 還執行了 myPrint($n++)
。 而後呢,纔是進入下一個 生成器
。
每一個 生成器(函數)
內的 變量
都有本身的棧空間,不受其餘 生成器
影響。 跳出當前生成器,變量的狀態依然存在,這個地方就有點像線程的感受,每一個線程也維持者本身的棧空間。因此,你會看到 $i = 0,1,2。。。都打印了3遍。
線程有本身獨佔的棧內存以及計數器。轉載著名出處: sifou
這裏打岔講一下 PHP.net goto.
PHP 中的 goto 有必定限制,目標位置只能位於同一個文件和做用域,也就是說沒法跳出一個函數或類方法,也沒法跳入到另外一個函數。也沒法跳入到任何循環或者 switch 結構中。能夠跳出循環或者 switch,一般的用法是用 goto 代替多層的 break。
因此 yield 雖然沒有 goto 靈活,可是比 goto 更強大, 能跳 循環,還能跨函數,做用域。
嗯,以上呢就是一個最簡單的形態任務調度器,你們先理解透徹了,再繼續往下看。
在複雜一點的 任務調度器,就拿鳥哥的轉載文章裏 在PHP中使用協程實現多任務調度。 的一個任務調度器來說吧,在文章中迭代了2個版本。代碼較多,而且代碼散落在文章中,我整理後放gitee scheduler了。你們能夠clone到本地運行試試。
鳥哥的文章已經講解得很清楚了,我就不多此一舉了,說說我我的感想吧。
文中的代碼使用了大量的 閉包,回調,引用。不少地方傳遞的是 一個個可執行的變量,理解起來有些燒腦。
咱們先看一下Java線程的生命週期, 以及PHP 生成器的狀態圖。
有不少類似的地方,接下來,咱們就嘗試用 PHP yield
實現一個 "類Java的多線程"
調度器。
代碼不少,放 gitee 了。
$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php
這個測試代碼,裏面用到了priority功能,能夠看到 t 須要個週期,t2 須要10個週期,因爲t2具備最高的執行優先級,在隨機調度過程當中,很快就執行完畢了。最後是 t 和 t3 (t3 須要運行8個週期)最後才執行完畢。
按照 Java 的實現,調用 一個線程的interrupt 方法時,會讓該線程,拋出一個異常,而PHP yield 有 throw 方法,我就依葫蘆畫瓢實現了。
$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php
代碼執行結果以下:
當 YieldThread
對象調用 sleep
方法後,5s內,任務調度輸出,就沒顯示 "線程1" 被執行的輸出。
我這代碼裏的 join,和wait是一個意思。等待線程執行完畢,不過尚未作
join(seconds)
這個功能。
$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php
執行效果以下
t3 生成器內 調用了t->join() 後,t3 在 t 沒執行前完畢以前,就沒有被調用過了。
而咱們的 主線程使用 wait(), 等待他們t,t4 倆都執行完畢後纔開始 輸出本身執行完畢的字符。
整個核心文件就:
能夠看到執行命令都是:$ php ./YieldBootstrap.php ./YieldSchedulerDemo1.php
。php 調用 YieldBootstrap.php
程序,自定義的代碼(demo代碼),是做爲參數傳入。在bootstrap
中,會對主程序作一個包裝—— MainYieldThread.php
包裹主 生成器
。而 用戶自定義的線程是繼承自 YieldThread.php
, 主線程,自線程,都繼承自 YieldThread
, 都放入到 YieldThreadScheduler.php
中,統一調度,這樣就實現了,線程切換。
這個"線程"的接口設計是照搬Java
的,原理實現呢,就按照Java-Thread
生命週期圖,以及PHP-yield
的活動狀態圖推演實現的。任務調度,優先級採用了輪盤,加隨機數實現的隨機調度。join
、wait
是經過一個數組記錄各個線程之間的依賴關係來判斷,當先線程是否ready
。
這個類多線程調度器,還不那麼完善,後續更新會放到 PHP yield thread
文字很少,代碼很長,很苦澀,你們下載到本地,多運行,多琢磨琢磨,必定能搞明白 yield
高級用法。歡迎留言,提問。