前言
相信大家都聽說過『協程』這個概念吧。
但是有些同學對這個概念似懂非懂,不知道怎么實現,怎么用,用在哪,甚至有些人認為yield就是協程!
我始終相信,如果你無法準確地表達出一個知識點的話,我可以認為你就是不懂。
如果你之前了解過利用PHP實現協程的話,你肯定看過鳥哥的那篇文章:在PHP中使用協程實現多任務調度| 風雪之隅
鳥哥這篇文章是從國外的作者翻譯來的,翻譯的簡潔明了,也給出了具體的例子了。
我寫這篇文章的目的,是想對鳥哥文章做更加充足的補充,畢竟有部分同學的基礎還是不夠好,看得也是云頭霧里的。
什么是協程
先搞清楚,什么是協程。
你可能已經聽過『進程』和『線程』這兩個概念。
進程就是二進制可執行文件在計算機內存里的一個運行實例,就好比你的.exe文件是個類,進程就是new出來的那個實例。
進程是計算機系統進行資源分配和調度的基本單位(調度單位這里別糾結線程進程的),每個CPU下同一時刻只能處理一個進程。
所謂的并行,只不過是看起來并行,CPU事實上在用很快的速度切換不同的進程。
進程的切換需要進行系統調用,CPU要保存當前進程的各個信息,同時還會使CPUCache被廢掉。
所以進程切換不到費不得已就不做。
那么怎么實現『進程切換不到費不得已就不做』呢?
首先進程被切換的條件是:進程執行完畢、分配給進程的CPU時間片結束,系統發生中斷需要處理,或者進程等待必要的資源(進程阻塞)等。你想下,前面幾種情況自然沒有什么話可說,但是如果是在阻塞等待,是不是就浪費了。
其實阻塞的話我們的程序還有其他可執行的地方可以執行,不一定要傻傻的等!
所以就有了線程。
線程簡單理解就是一個『微進程』,專門跑一個函數(邏輯流)。
所以我們就可以在編寫程序的過程中將可以同時運行的函數用線程來體現了。
線程有兩種類型,一種是由內核來管理和調度。
我們說,只要涉及需要內核參與管理調度的,代價都是很大的。這種線程其實也就解決了當一個進程中,某個正在執行的線程遇到阻塞,我們可以調度另外一個可運行的線程來跑,但是還是在同一個進程里,所以沒有了進程切換。
還有另外一種線程,他的調度是由程序員自己寫程序來管理的,對內核來說不可見。這種線程叫做『用戶空間線程』。
協程可以理解就是一種用戶空間線程。
協程,有幾個特點:
- 協同,因為是由程序員自己寫的調度策略,其通過協作而不是搶占來進行切換
- 在用戶態完成創建,切換和銷毀
- ⚠️ 從編程角度上看,協程的思想本質上就是控制流的主動讓出(yield)和恢復(resume)機制
- 迭代器經常用來實現協程
說到這里,你應該明白協程的基本概念了吧?
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"; }
生成器
可以說之前為了擁有一個能夠被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協程
前面介紹協程的時候說了,協程需要程序員自己去編寫調度機制,下面我們來看這個機制怎么寫。
0)生成器正確使用
既然生成器不能像函數一樣直接調用,那么怎么才能調用呢?
方法如下:
- foreach他
- send($value)
- current / next...
1)Task實現
Task就是一個任務的抽象,剛剛我們說了協程就是用戶空間協程,線程可以理解就是跑一個函數。