摘要:把中的初始化方法改下,因為我們在運行一個的時候,我們要分析出他包含了哪些子協(xié)程,然后將子協(xié)程用一個堆棧保存。總結這下應該明白怎么實現協(xié)程了吧建議不要使用的來實現協(xié)程,推薦使用,已經支持了協(xié)程,并附帶了部分案例。
前言
相信大家都聽說過『協(xié)程』這個概念吧。
但是有些同學對這個概念似懂非懂,不知道怎么實現,怎么用,用在哪,甚至有些人認為yield就是協(xié)程!
我始終相信,如果你無法準確地表達出一個知識點的話,我可以認為你就是不懂。
如果你之前了解過利用PHP實現協(xié)程的話,你肯定看過鳥哥的那篇文章:在PHP中使用協(xié)程實現多任務調度| 風雪之隅
鳥哥這篇文章是從國外的作者翻譯來的,翻譯的簡潔明了,也給出了具體的例子了。
我寫這篇文章的目的,是想對鳥哥文章做更加充足的補充,畢竟有部分同學的基礎還是不夠好,看得也是云頭霧里的。
什么是協(xié)程歡迎微博關注我 @碼云 。2. 全互聯網最全的PHP技能圖譜:https://bruceit.com/skills
先搞清楚,什么是協(xié)程。
你可能已經聽過『進程』和『線程』這兩個概念。
進程就是二進制可執(zhí)行文件在計算機內存里的一個運行實例,就好比你的.exe文件是個類,進程就是new出來的那個實例。
進程是計算機系統(tǒng)進行資源分配和調度的基本單位(調度單位這里別糾結線程進程的),每個CPU下同一時刻只能處理一個進程。
所謂的并行,只不過是看起來并行,CPU事實上在用很快的速度切換不同的進程。
進程的切換需要進行系統(tǒng)調用,CPU要保存當前進程的各個信息,同時還會使CPUCache被廢掉。
所以進程切換不到非不得已就不做。
那么怎么實現『進程切換不到非不得已就不做』呢?
首先進程被切換的條件是:進程執(zhí)行完畢、分配給進程的CPU時間片結束,系統(tǒng)發(fā)生中斷需要處理,或者進程等待必要的資源(進程阻塞)等。你想下,前面幾種情況自然沒有什么話可說,但是如果是在阻塞等待,是不是就浪費了。
其實阻塞的話我們的程序還有其他可執(zhí)行的地方可以執(zhí)行,不一定要傻傻的等!
所以就有了線程。
線程簡單理解就是一個『微進程』,專門跑一個函數(邏輯流)。
所以我們就可以在編寫程序的過程中將可以同時運行的函數用線程來體現了。
線程有兩種類型,一種是由內核來管理和調度。
我們說,只要涉及需要內核參與管理調度的,代價都是很大的。這種線程其實也就解決了當一個進程中,某個正在執(zhí)行的線程遇到阻塞,我們可以調度另外一個可運行的線程來跑,但是還是在同一個進程里,所以沒有了進程切換。
還有另外一種線程,他的調度是由程序員自己寫程序來管理的,對內核來說不可見。這種線程叫做『用戶空間線程』。
協(xié)程可以理解就是一種用戶空間線程。
協(xié)程,有幾個特點:
協(xié)同,因為是由程序員自己寫的調度策略,其通過協(xié)作而不是搶占來進行切換
在用戶態(tài)完成創(chuàng)建,切換和銷毀
?? 從編程角度上看,協(xié)程的思想本質上就是控制流的主動讓出(yield)和恢復(resume)機制
generator經常用來實現協(xié)程
說到這里,你應該明白協(xié)程的基本概念了吧?
PHP實現協(xié)程一步一步來,從解釋概念說起!
可迭代對象PHP5提供了一種定義對象的方法使其可以通過單元列表來遍歷,例如用foreach語句。
你如果要實現一個可迭代對象,你就要實現Iterator接口:
var = $array; } } public function rewind() { echo "rewinding "; reset($this->var); } public function current() { $var = current($this->var); echo "current: $var "; return $var; } public function key() { $var = key($this->var); echo "key: $var "; return $var; } public function next() { $var = next($this->var); echo "next: $var "; return $var; } public function valid() { $var = $this->current() !== false; echo "valid: {$var} "; return $var; } } $values = array(1,2,3); $it = new MyIterator($values); foreach ($it as $a => $b) { print "$a: $b "; }生成器
可以說之前為了擁有一個能夠被foreach遍歷的對象,你不得不去實現一堆的方法,yield關鍵字就是為了簡化這個過程。
生成器提供了一種更容易的方法來實現簡單的對象迭代,相比較定義類實現Iterator接口的方式,性能開銷和復雜性大大降低。
記住,一個函數中如果用了yield,他就是一個生成器,直接調用他是沒有用的,不能等同于一個函數那樣去執(zhí)行!
所以,yield就是yield,下次誰再說yield是協(xié)程,我肯定把你xxxx。
PHP協(xié)程前面介紹協(xié)程的時候說了,協(xié)程需要程序員自己去編寫調度機制,下面我們來看這個機制怎么寫。
0)生成器正確使用既然生成器不能像函數一樣直接調用,那么怎么才能調用呢?
方法如下:
foreach他
send($value)
current / next...
1)Task實現Task就是一個任務的抽象,剛剛我們說了協(xié)程就是用戶空間線程,線程可以理解就是跑一個函數。
所以Task的構造函數中就是接收一個閉包函數,我們命名為coroutine。
/** * 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執(zhí)行完畢了沒有 * * @return bool */ public function isFinished() { return !$this->coroutine->valid(); } /** * 設置下次要傳給協(xié)程的值,比如 $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; } } }2)Scheduler實現接下來就是Scheduler這個重點核心部分,他扮演著調度員的角色。
/** * Class Scheduler */ Class Scheduler { /** * @var SplQueue */ protected $taskQueue; /** * @var int */ protected $tid = 0; /** * Scheduler constructor. */ public function __construct() { /* 原理就是維護了一個隊列, * 前面說過,從編程角度上看,協(xié)程的思想本質上就是控制流的主動讓出(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); // 任務如果還沒完全執(zhí)行完畢,入隊等下次執(zhí)行 } } } }這樣我們基本就實現了一個協(xié)程調度器。
你可以使用下面的代碼來測試:
addTask(task1()); // 添加不同的閉包函數作為任務 $scheduler->addTask(task2()); $scheduler->run();關鍵說下在哪里能用得到PHP協(xié)程。
function task1() { /* 這里有一個遠程任務,需要耗時10s,可能是一個遠程機器抓取分析遠程網址的任務,我們只要提交最后去遠程機器拿結果就行了 */ remote_task_commit(); // 這時候請求發(fā)出后,我們不要在這里等,主動讓出CPU的執(zhí)行權給task2運行,他不依賴這個結果 yield; yield (remote_task_receive()); ... } function task2() { for ($i = 1; $i <= 5; ++$i) { echo "This is task 2 iteration $i. "; yield; // 主動讓出CPU的執(zhí)行權 } }這樣就提高了程序的執(zhí)行效率。
關于『系統(tǒng)調用』的實現,鳥哥已經講得很明白,我這里不再說明。
3)協(xié)程堆棧鳥哥文中還有一個協(xié)程堆棧的例子。
我們上面說過了,如果在函數中使用了yield,就不能當做函數使用。
所以你在一個協(xié)程函數中嵌套另外一個協(xié)程函數:
addTask(task()); $scheduler->run();這里的echoTimes是執(zhí)行不了的!所以就需要協(xié)程堆棧。
不過沒關系,我們改一改我們剛剛的代碼。
把Task中的初始化方法改下,因為我們在運行一個Task的時候,我們要分析出他包含了哪些子協(xié)程,然后將子協(xié)程用一個堆棧保存。(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()的時候,一個循環(huán)來分析:
/** * @param Generator $gen */ function stackedCoroutine(Generator $gen) { $stack = new SplStack; // 不斷遍歷這個傳進來的生成器 for (; ;) { // $gen可以理解為指向當前運行的協(xié)程閉包函數(生成器) $value = $gen->current(); // 獲取中斷點,也就是yield出來的值 if ($value instanceof Generator) { // 如果是也是一個生成器,這就是子協(xié)程了,把當前運行的協(xié)程入棧保存 $stack->push($gen); $gen = $value; // 把子協(xié)程函數給gen,繼續(xù)執(zhí)行,注意接下來就是執(zhí)行子協(xié)程的流程了 continue; } // 我們對子協(xié)程返回的結果做了封裝,下面講 $isReturnValue = $value instanceof CoroutineReturnValue; // 子協(xié)程返回`$value`需要主協(xié)程幫忙處理 if (!$gen->valid() || $isReturnValue) { if ($stack->isEmpty()) { return; } // 如果是gen已經執(zhí)行完畢,或者遇到子協(xié)程需要返回值給主協(xié)程去處理 $gen = $stack->pop(); //出棧,得到之前入棧保存的主協(xié)程 $gen->send($isReturnValue ? $value->getValue() : NULL); // 調用主協(xié)程處理子協(xié)程的輸出值 continue; } $gen->send(yield $gen->key() => $value); // 繼續(xù)執(zhí)行子協(xié)程 } }然后我們增加echoTime的結束標示:
class CoroutineReturnValue { protected $value; public function __construct($value) { $this->value = $value; } // 獲取能把子協(xié)程的輸出值給主協(xié)程,作為主協(xié)程的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 "; yield; } yield retval(""); // 增加這個作為結束標示 }Task變?yōu)椋?/p>
function task1() { yield echoTimes("bar", 5); }這樣就實現了一個協(xié)程堆棧,現在你可以舉一反三了。
4)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 "; yield; } }task1生成器:
function task1() { yield from echoTimes("bar", 5); }這樣,輕松調用子協(xié)程。
總結這下應該明白怎么實現PHP協(xié)程了吧?
建議不要使用PHP的Yield來實現協(xié)程,推薦使用swoole,2.0已經支持了協(xié)程,并附帶了部分案例。End...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/26223.html
摘要:本文先回顧生成器,然后過渡到協(xié)程編程。其作用主要體現在三個方面數據生成生產者,通過返回數據數據消費消費者,消費傳來的數據實現協(xié)程。解決回調地獄的方式主要有兩種和協(xié)程。重點應當關注控制權轉讓的時機,以及協(xié)程的運作方式。 轉載請注明文章出處: https://tlanyan.me/php-review... PHP回顧系列目錄 PHP基礎 web請求 cookie web響應 sess...
摘要:協(xié)程完全有用戶態(tài)程序控制,所以也被成為用戶態(tài)的線程。目前支持協(xié)程的語言有很多,例如等。協(xié)程之旅前篇結束,下一篇文章我們將深入分析原生協(xié)程部分的實現。 寫在最前 ??Swoole協(xié)程經歷了幾個里程碑,我們需要在前進的道路上不斷總結與回顧自己的發(fā)展歷程,正所謂溫故而知新,本系列文章將分為協(xié)程之旅前、中、后三篇。 前篇主要介紹協(xié)程的概念和Swoole幾個版本協(xié)程實現的主要方案技術; 中篇主...
摘要:搶占式調度我們在今年年初就計劃實現的搶占式調度,以滿足實現有些場景下的不均衡調度帶來的問題。考慮開線程,負責檢查當前執(zhí)行協(xié)程執(zhí)行時間。達到我們的第二個協(xié)程主動搶占第一個協(xié)程的效果。 前言 Swoole內核團隊開設的專欄,會逐漸投入精力寫文章介紹Swoole的開發(fā)歷程,實現原理,應用實踐等,大家可以更好的交流,共同學習,建設PHP生態(tài)。 協(xié)程調度 去年Swoole推出了4.0版本后,完整...
摘要:協(xié)程其實就是一個可中途中斷,由外部來控制執(zhí)行進程的函數。這些第三方的選擇的共同特點是協(xié)程的都是隱式的。這就是顯示控制和隱式控制的區(qū)別。本文討論的協(xié)程就是這一種,后面會逐漸展開到如何利用這種顯示控制的協(xié)程來解決阻塞和流程阻塞的問題。 Python官方的實現里,協(xié)程只有generator這一招。協(xié)程其實就是一個可中途中斷,由外部來控制執(zhí)行進程的函數。除了官方的generator,還有很多第...
閱讀 1258·2021-11-19 09:40
閱讀 3117·2021-11-02 14:47
閱讀 3049·2021-10-11 10:58
閱讀 3216·2019-08-30 15:54
閱讀 2666·2019-08-30 12:50
閱讀 1721·2019-08-29 16:54
閱讀 462·2019-08-29 15:38
閱讀 1237·2019-08-29 15:19