Swoole從入門到入土(8)——協程初探
這一章節“協程”話題的討論是為了讓我們對之後協程風格服務端有更全面的瞭解。所以我們需要先一起了解一下什麼是協程?協程有什麼作用?
當大家第一次看到“協程”這個詞的時候,應該都一樣會開啟某度、某歌搜尋一翻,然後搜到一堆很玄幻的概念,比如以下這一句:“協程(coroutine)也是一種程式元件。相對子例程而言,協程更為一般和靈活,但在實踐中使用沒有子例程那樣廣泛。”往往讓人看了一臉問號-_-
其實“協程”這東西,我們可以這麼理解,程序是個容器,對應具體幹活的是執行緒;同樣的道理,我們可以把執行緒看成是一個容器,協程是執行緒中具體幹活的過程。換種說法,可以把協程想象成一個個函式(每個函式的作用不一樣),而協程就是在同一個時間段內讓這些函式同時工作。
協程最主要的作用是:讓原來要使用非同步+回撥方式寫的非人類程式碼,可以用看似同步的方式寫出來(即:按序列模型去組織原本分散在不同上下文中的程式碼邏輯)。
網上有一些觀點,說協程可以很好解決IO瓶頸問題(因為這時執行緒發揮不了作用)。編者只想說,IO 是系統呼叫,這個 IO 不是使用者態能處理的,協程 是沒辦法繞開的,所以最終還是給堵了。如果 協程 真的能處理堵塞問題,那麼很多經典的 Unix 網路程式設計書籍裡面應該有 多協程 模式才對。
編者作為IT界的泥石流,習慣了非同步的寫法,對於協程抱著接受但是審視的態度。今天我們就一起看看swoole下的協程式設計。
我們先看看一段簡易的協程程式碼:
echo "main start",PHP_EOL; Co\run(function(){ echo "co 1 start",PHP_EOL; go(function(){ echo "co 2 start",PHP_EOL;echo "co 2 end",PHP_EOL; }); go(function(){ echo "co 3 start",PHP_EOL;echo "co 3 end",PHP_EOL; }); echo "co 1 end",PHP_EOL; }); echo "end";
這段程式碼的執行結果是:
main start
co 1 start
co 2 start
co 2 end
co 3 start
co 3 end
co 1 end
end
好像也沒什麼嘛,就是程式碼順序執行的結果。先不要急,我們一起看看程式碼中比較陌生的部分:
Co\run():在Swoole直接裸寫協程啟動,就需要呼叫這個函式(其實是對 Swoole\Coroutine\Scheduler 類 (協程排程器類) 的封裝),可以理解為C語言裡的main()函式。(另外,Swoole 提供的 2 個程序管理模組 Process 和 Process\Pool 的 start 方法,此種啟動方式會在程序啟動的時候建立協程容器,參考這兩個模組建構函式的 enable_coroutine 引數)。
go():新增一個子協程。
現在,我們把程式碼修改如下(添加了Co::sleep)
echo "main start",PHP_EOL; Co\run(function(){ echo "co 1 start",PHP_EOL; go(function(){ echo "co 2 start",PHP_EOL; co::sleep(1); echo "co 2 end",PHP_EOL; }); go(function(){ echo "co 3 start",PHP_EOL; co::sleep(.5); echo "co 3 end",PHP_EOL; }); echo "co 1 end",PHP_EOL; }); echo "end";
這時的結果會變成:
main start
co 1 start
co 2 start
co 3 start
co 1 end
co 3 end
co 2 end
end
是不是很神奇:)根據等待的時間,協程會自動排程現在可以馬上就處理的程式碼片斷。嗯,這就是協程最大的優勢。
我們再做一個實驗,把上面的co::sleep()函式換成php自帶的sleep()函式會發生什麼事?
echo "main start",PHP_EOL; Co\run(function(){ echo "co 1 start",PHP_EOL; go(function(){ echo "co 2 start",PHP_EOL; sleep(1); //注意這裡 echo "co 2 end",PHP_EOL; }); go(function(){ echo "co 3 start",PHP_EOL; sleep(3); //注意這裡 echo "co 3 end",PHP_EOL; }); echo "co 1 end",PHP_EOL; }); echo "end";
這時,我們得到的結果同樣會是:
main start
co 1 start
co 2 start
co 2 end
co 3 start
co 3 end
co 1 end
end
區別在於在co 2 start和co 3 start會被阻塞等待。究其原因就是在co::sleep()內部會用yield把時間片讓出來,而sleep()則是在系統層面等待。這就是說為什麼系統IO是協程繞不開的原因,該等的還是得等,只不過用了一些技巧是在其它地方等而已。
這就給了我們一些啟發,如果用協程程式設計,碰到需要阻塞的部分(比如sleep、http請求、mysql連線、檔案讀寫),需要用swoole為我們提供的現在協程庫;如果協程庫不夠用,則利用非同步原理與協程庫實現自己的協程元件。
但是,swoole已經考慮到程式設計師中不乏槓精,就想把上面這請阻塞操作變成協程,只需如下操作就可以實現把程式碼“一鍵協程化”:
Swoole\Runtime::enableCoroutine(); //重點在這一句 echo "main start",PHP_EOL; Co\run(function(){ echo "co 1 start",PHP_EOL; go(function(){ echo "co 2 start",PHP_EOL; sleep(2); echo "co 2 end",PHP_EOL; }); go(function(){ echo "co 3 start",PHP_EOL; sleep(1); echo "co 3 end",PHP_EOL; }); echo "co 1 end",PHP_EOL; }); echo "end";
雖然swoole提供了“一鍵協程化”的神仙操作,可以把檔案操作,sleep,Mysqli,PDO,streams等都變成非同步IO,但是要注意,底層是使用了HOOK的方式把原PHP的程式碼呼叫轉移到了swoole的函式內,可參考這裡。所以認識swoole提供的原生協程庫也是非常重要的。
接下來,再來一段程式碼:
echo "main start",PHP_EOL; echo "co 1 start",PHP_EOL; go(function(){ echo "co 2 start",PHP_EOL; co::sleep(1); echo "co 2 end",PHP_EOL; }); go(function(){ echo "co 3 start",PHP_EOL; co::sleep(.5); echo "co 3 end",PHP_EOL; }); echo "co 1 end",PHP_EOL; echo "end",PHP_EOL;
我們得到的結果是:
main start
co 1 start
co 2 start
co 3 start
co 1 end
end
co 2 end
co 2 end
在這一段程式碼中,沒有協程容器Co\run()的存在,go()子協程同樣會被呼叫。可以看出協程容器可以保證容器內的協程程式碼全部執行完成後,再跳出容器往下執行外部的程式碼。
好了,協程初探就先到這裡了。接下去,我們會先了解協程版本的TCP伺服器。關於swoole協程的其它話題,我們後續會進行討論。
--------------------------- 我是可愛的分割線 ----------------------------
最後博主借地宣傳一下,漳州程式設計小組招新了,這是一個面向漳州青少年資訊學/軟體設計的學習小組,有意向的同學點選連結,聯絡我吧。