首先是,這是我第一次把公眾號文章複製粘貼到sf.gg來。
其次是,很久很久之前,我挖了一個yield的一個坑,自己挖的坑自己填,不然遲早會把自己埋掉。
最後是,如果想看之前那個坑,請發送“yield”給文章末尾的公眾號,我開通了高大上的自動回覆功能,稀罕地不得了!
PS:那篇文章中在最後我犯了一個錯誤,誤下了一個結論:foreach中不能使用send並猜測這是PHP的bug,實際上並不是,真實的原因粗暴簡單的理解就是send會讓生成器繼續執行一次導致。這件事情告訴我們:
除了裝逼之外,甩鍋也是有打臉風險的
那篇坑裏,內容和你能在百毒上搜索到的大多數文章都是差不多的,不過我那篇坑標題起得好:《yield是個什麼玩意(上)》,也就是暗示大家還有下篇,所以起標題也是需要一定技術含量的。
我堅信,在座的各位辣雞在看完上篇坑文後最想説的註定是泰迪熊這句話(這是文化屬性,不以各位的意志而轉移):
回到今天主旨上來,強調幾點:
- 雖然文章標題中有“yield和協程”這樣的關鍵字,但實際上yield並不是協程,看起來有不少人直接將yield和協程劃了等號。yield的本質是生成器,英文名字叫做Generator。
- yield只能用在function中,但用了yield就已經不是傳統意義上的function了,同時如果你企圖在function之外的其他地方用yield,你會被打臉。
- yield的最重要作用就是:自己中斷一坨代碼的執行,然後主動讓出CPU控制權給路人甲;然後又能通過一些方式從剛才中斷的地方恢復運行。這個就比較屌了,假如你請求了一個費時10s的服務器API,此時是可以讓出CPU給路人甲。粗暴地説上面的過程就算是協程的基本概念。
多線程和多進程都是操作系統參與的調度,而協程是用户自主實現的調度,協程的關鍵點實際上是“用户層實現自主調度”,大概有“翻身農奴把歌唱”的意思。
下面我通過一坨代碼來體會一把“翻身農奴”,你們感受一下:
<?php
// 本代碼段僅用於demo
function gen1() {
for( $i = 1; $i <= 10; $i++ ) {
echo "GEN1 : {$i}".PHP_EOL;
// sleep沒啥意思,主要就是運行時候給你一種切實的調度感,你懂麼
// 就是那種“你看!你看!尼瑪,我調度了!卧槽”
sleep( 1 );
// 這句很關鍵,表示自己主動讓出CPU,我不下地獄誰下地獄
yield;
}
}
function gen2() {
for( $i = 1; $i <= 10; $i++ ) {
echo "GEN2 : {$i}".PHP_EOL;
// sleep沒啥意思,主要就是運行時候給你一種切實的調度感,你懂麼
// 就是那種“你看!你看!尼瑪,我調度了!卧槽”
sleep( 1 );
// 這句很關鍵,表示自己主動讓出CPU,我不下地獄誰下地獄
yield;
}
}
$task1 = gen1();
$task2 = gen2();
while( true ) {
// 首先我運行task1,然後task1主動下了地獄
echo $task1->current();
// 這會兒我可以讓task2介入進來了
echo $task2->current();
// task1恢復中斷
$task1->next();
// task2恢復中斷
$task2->next();
}
上面代碼執行結果如下圖:
雖然我話都説到這裏了,但是肯定還是有人get不到“所以,到底發生了什麼?”。你要知道,如果function gen1和function gen2中沒有yield,而是普通函數,你是無法中斷其中的for循環的,諸如下面這樣的代碼:
本代碼段僅用於demo
<?php
function gen1() {
for( $i = 1; $i <= 10; $i++ ) {
echo "GEN1 : {$i}".PHP_EOL;
sleep( 1 );
}
}
function gen2() {
for( $i = 1; $i <= 10; $i++ ) {
echo "GEN2 : {$i}".PHP_EOL;
}
}
gen1();
gen2();
// 看這裏,看這裏,看這裏!
// 上面的代碼一旦運行,一定是先運行完gen1函數中的for循環
// 其次才能運行完gen2函數中的for循環,絕對不會出現
// gen1和gen2交叉運行這種情況
我似乎已然精通了yield
寫到這裏後我也開始蹩了,和以往的憋了三天蹦不出來個屁有所不同,我這次蹩出了一個比較典型的應用場景:curl。下面我們基於上面那坨辣雞代碼將gen1修改為一個耗時curl網絡請求,gen2將向一個文本文件中寫內容,我們的目的就是在耗時的curl開始後主動讓出CPU,讓gen2去寫文件,以實現CPU的最大化利用。
本代碼段僅用於demo
<?php
$ch1 = curl_init();
// 這個地址中的php,我故意sleep了5秒鐘,然後輸出一坨json
curl_setopt( $ch1, CURLOPT_URL, "http://www.selfctrler.com/index.php/test/test1" );
curl_setopt( $ch1, CURLOPT_HEADER, 0 );
$mh = curl_multi_init();
curl_multi_add_handle( $mh, $ch1 );
function gen1( $mh, $ch1 ) {
do {
$mrc = curl_multi_exec( $mh, $running );
// 請求發出後,讓出cpu
yield;
} while( $running > 0 );
$ret = curl_multi_getcontent( $ch1 );
echo $ret.PHP_EOL;
return false;
}
function gen2() {
for ( $i = 1; $i <= 10; $i++ ) {
echo "gen2 : {$i}".PHP_EOL;
file_put_contents( "./yield.log", "gen2".$i, FILE_APPEND );
yield;
}
}
$gen1 = gen1( $mh, $ch1 );
$gen2 = gen2();
while( true ) {
echo $gen1->current();
echo $gen2->current();
$gen1->next();
$gen2->next();
}
上面的代碼,運行以後,我們再等待curl發起請求的5秒鐘內,同時可以完成文件寫入功能,如果換做平時的PHP程序,就只能是先阻塞等待curl拿到結果後才能完成文件寫入。
文章太長,就像“老太太的裹腳布一樣,又臭又長”,所以,最後再對代碼做個極小幅度的改動就收尾不寫了!
本代碼段僅用於demo
<?php
$ch1 = curl_init();
// 這個地址中的php,我故意sleep了5秒鐘,然後輸出一坨json
curl_setopt( $ch1, CURLOPT_URL, "http://www.selfctrler.com/index.php/test/test1" );
curl_setopt( $ch1, CURLOPT_HEADER, 0 );
$mh = curl_multi_init();
curl_multi_add_handle( $mh, $ch1 );
function gen1( $mh, $ch1 ) {
do {
$mrc = curl_multi_exec( $mh, $running );
// 請求發出後,讓出cpu
$rs = yield;
echo "外部發送數據{$rs}".PHP_EOL;
} while( $running > 0 );
$ret = curl_multi_getcontent( $ch1 );
echo $ret.PHP_EOL;
return false;
}
function gen2() {
for ( $i = 1; $i <= 10; $i++ ) {
echo "gen2 : {$i}".PHP_EOL;
file_put_contents( "./yield.log", "gen2".$i, FILE_APPEND );
$rs = yield;
echo "外部發送數據{$rs}".PHP_EOL;
}
}
$gen1 = gen1( $mh, $ch1 );
$gen2 = gen2();
while( true ) {
echo $gen1->current();
echo $gen2->current();
$gen1->send("gen1");
$gen2->send("gen2");
}
我們修改了內容:
將$gen1->next()修改成了$gen1->send("gen1")
在function gen1中yield有了返回值,並且將返回值打印出來
這件事情告訴我們:yield和send,是可以雙向通信的,同時告訴我們send可以用來恢復原來中斷的代碼,而且在恢復中斷的同時可以攜帶信息回去。寫到這裏,你是不是覺得這玩意的可利用價值是不是比原來高點兒了?
我知道,有人肯定叨叨了:“老李,你代碼特麼寫的真是辣雞啊!你之前保證過了的 --- 只在公司生產環境寫辣雞代碼的。可你看看你這辣雞光環到籠罩都到demo裏了,你連demo都不放過了!你怎麼説?!”。兄dei,“又不是不能用”。而且我告訴你,上面這點兒curl demo來講明白yield還是不夠的,後面還有兩三篇yield呢,照樣是爛代碼噁心死你,愛看不看。我勸你心放寬,你想想你這麼爛的代碼都經歷了,還有什麼不能經歷的?
文章最後補個小故事:其實yield是PHP 5.5就已經添加進來了,這個模塊的作者叫做Nikita Popov,網絡上的名稱是Nikic。我們知道PHP7這一代主力是惠新宸,下一代PHP主力就是Nikic了。早在2012年,Nikic就發表了一篇關於PHP yield多任務的文章,鏈接我貼出來大家共賞一下 --- http://nikic.github.io/2012/1...
----- 分割線 ----
重要提示
重要提示
重要提示:前面説過了使用yield可以臨時讓出CPU,在demo中我們我使用了curlmultiexec()函數對yield的這種讓出CPU的特性進行了説明,但此處需要注意,這種場合中不要使用帶有阻塞性質的函數(實際上絕大部分PHP函數直接滅絕了,它只能通過嘗試結合類似於epoll方案的才能進一步發揮。包括本文demo中用的curlmultiexec()最好也應該搭配curlmultiselect()其實才是最佳,而curlmultiselect()底層是調用select IO複用)。之前我還對與一些博主在demo代碼段上備註【本demo僅用於demo】這種語句感到多餘,現如今看以後還是謹慎得的好。
本提示的產生來源於文章的一個評論,【誤人子弟】的黑鍋我先甩出去了承擔不了,但文章確實缺少了這個重要提示,是我疏忽大意,特此感謝。