一直讓 PHP 程序員懵逼的同步阻塞異步非阻塞,終于搞明白了
大家好,我是碼農(nóng)先森。
經(jīng)常聽到身邊寫 Java、Go 的朋友提到程序異步、非阻塞、線程、協(xié)程,讓系統(tǒng)性能提高到百萬、千萬并發(fā),使我甚是驚訝屬實羨慕。對于常年寫 PHP 的我來說,最初聽到這幾個詞時,腦袋一直處于蒙圈狀態(tài),回過頭來看著自己手上同步阻塞的 PHP 代碼,心想著「異步、非阻塞、線程、協(xié)程」到底是個什么東東,這么厲害嘛。其實 PHP 中也有線程、協(xié)程,但在日常的編程中幾乎不會使用,原因是 PHP-FPM 多進程模式下并不支持線程、協(xié)程,使用 PHP 編程的程序員絕大多數(shù)都離不開 PHP-FPM 。這也就導致了 PHP 程序員對那些概念沒有接觸,那就更別提理解了,因此為了廣大的 PHP 程序員同胞們能夠和 Java、Go 的程序員對上話,特地對「同步、阻塞、異步、非阻塞」這幾個概念進行了深度的分析,爭取把 PHP 程序員的腰桿挺直溜。
按照慣例先上八股文這道菜:
- 同步阻塞:當一個操作被調(diào)用時,調(diào)用者將被阻塞,直到這個操作完成并返回結果。在此期間,調(diào)用者無法進行其他任務。
- 異步阻塞:當一個操作被調(diào)用時,調(diào)用者不會被阻塞,而是可以繼續(xù)執(zhí)行其他任務。然而,它仍然需要等待被調(diào)用的操作完成,并在操作完成后處理其結果。這個等待過程可能是阻塞的。
- 同步非阻塞:調(diào)用者發(fā)起一個操作后,不會被阻塞并可以繼續(xù)執(zhí)行其他任務。雖然調(diào)用者可以立即獲得控制權,但它仍然需要等待操作完成才能處理結果。在等待的過程中,調(diào)用者可以主動輪詢或者不斷嘗試獲取操作結果,以避免長時間的阻塞。
- 異步非阻塞:調(diào)用者發(fā)起一個操作后,不會被阻塞并可以繼續(xù)執(zhí)行其他任務。同時,調(diào)用者也不需要等待操作完成來處理結果。相反,調(diào)用者可以注冊一個回調(diào)函數(shù)或者使用類似事件驅(qū)動的機制,當操作完成后被自動觸發(fā)回調(diào)函數(shù)來處理結果。
基礎知識扎實的朋友看這個八股文就足以解惑了,不過看得懂八股文的畢竟是少數(shù)英俊帥氣人,你說氣不氣人集顏值與才華于一體,別看說的就是各位看官「哈哈」。言歸正傳,那看不懂八股文的怎么搞?別急,且聽我結合生活中的例子娓娓道來。
你每天上班匆匆路過的早餐店,今天額外的多人,你湊近一看原來是來了位身材高挑楚楚動人的美女服務員,結果你按耐不住心中的激動,今天高低得買兩個饅頭外加一杯豆?jié){,由于買的人太多,蒸好的饅頭早已賣完,這時你只能等正在蒸的,期間你什么也干不了只能眼勾勾的干等著,那么這時的你是同步阻塞的。
由于來買早餐的人越來越多,離上班的時間也越來越近,你開始了騷動,每隔幾分鐘就問美女服務員饅頭蒸好了沒?此時的你不再干等,而是開始刷刷抖音看看工作群,因為你已經(jīng)付錢了所以還是得等饅頭,由于美女服務員太忙了沒空主動告訴你,需要你自己不斷地問,那么這時的你是同步非阻塞的。
過了高峰期人變少了,視野更廣闊了,你看美女服務員更清楚了,結果你又開始眼勾勾的干等著,抖音也不刷了工作群的消息也不顧了。由于美女服務員不忙了,開始主動叫那位身穿格子衫背雙肩包帥哥,饅頭蒸好了,這時的你甩了甩頭上的劉海,接過了美女服務員手中的饅頭會心一笑,順便還加了對方的微信,那么此時的你是異步阻塞的。
隔天你為了再睹芳容,又來到了這家早餐店,一向摳門的你甩手就點了兩個肉包。這時美女服務員迎面笑臉告知你肉包還需耐心等待哦,蒸好了會微信通知你。在炎炎的夏日里你路上走的太匆忙,此時的你口渴難耐,就去隔壁小賣部買了瓶82年的可樂,還坐著吹了會空調(diào)。隨著微信的一聲叮咚,你起身去早餐店,接過了美女服務員手中的肉包,那么此時的你是異步非阻塞的。
有了美女服務員的投喂,你工作的干勁都十足了,同時應該也把「同步、阻塞、異步、非阻塞」這幾個概念搞懂了吧。其實這里的同步異步和阻塞非阻塞,容易搞混淆就像你看美女服務員容易丟魂一樣,在這個例子中同步異步需要關注的是「美女服務員是否會主動的通知你」,主動通知你那么對你來說就是異步的,需要你去詢問那么對你來說就是同步的。阻塞非阻塞需要關注的點是「你是否是眼勾勾的干等著」,如果你只能干等那就是阻塞的,如果你還能干點其他的事情比如刷抖音、買82年的可樂,那么就是非阻塞的。
美女也看了道理也懂了,有的朋友們又要產(chǎn)生新的疑問了,那在程序中怎么體現(xiàn)、怎么用「同步、阻塞、異步、非阻塞」呢?那我們就開始上代碼,畢竟看美女服務員的目的也是為了能夠深入交往嘛,也就等同于實踐上手了,你細品是不是這個理。
開整!
我們先來看同步阻塞的例子,使用 socket_create、socket_bind、socket_listen 函數(shù)創(chuàng)建綁定并監(jiān)聽了 8080 端口,然后一直阻塞在 socket_accept 函數(shù)上,直到有客戶端連接的到來。傳統(tǒng)的 PHP-FPM 就是同步阻塞的模式,不過 PHP-FPM 多進程模型,在接收到客戶端連接 $client 后就交給由子進程進行后續(xù)的處理了,在這個例子只舉例了單進程的模式。
<?php
// 同步阻塞模式
// 創(chuàng)建一個監(jiān)聽 Socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 綁定 8080 端口
socket_bind($socket, '0.0.0.0', 8080);
// 開始監(jiān)聽
socket_listen($socket);
while(true){
// 會阻塞在這里,一直等著客戶端來連接
// 結合剛剛的例子可以理解為,你一直在這里眼勾勾的干等饅頭,啥也干不了
$client = socket_accept($socket);
if($client){
echo "客官來了" . PHP_EOL;
}
}
再來看看同步非阻塞的例子,同樣也是監(jiān)聽了 8080 端口,不同的是將套接字 $socket 設置成了非阻塞模式。那么這種情況下將不會一直阻塞在 socket_accept 函數(shù)上,會繼續(xù)往下執(zhí)行,如果沒有寫其他的邏輯,就會出現(xiàn)放空炮的現(xiàn)象。這種模式在實際的編程中基本上不會采用,會把系統(tǒng)榨干,這一點值得注意一下,誰寫了這樣的代碼就要拉出去罰站了。
<?php
// 同步非阻塞模式
// 創(chuàng)建一個監(jiān)聽 Socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 綁定 8080 端口
socket_bind($socket, '0.0.0.0', 8080);
// 開始監(jiān)聽
socket_listen($socket);
// 這里設置成非阻塞
socket_set_nonblock($socket);
while(true){
// 不會阻塞在這里
$client = socket_accept($socket);
if($client){
echo "客官來了" . PHP_EOL;
}
// 會繼續(xù)往下執(zhí)行
// 結合剛剛的例子,可以在這里刷刷抖音、看看工作群消息等
// ...
// 如果你上面沒有寫任何的邏輯,這些最好 sleep 一下
// 不然 CPU 就會被榨干了,也就是說不要一直眼勾勾的盯著美女服務員會被吸干
// 要適當?shù)男菹⒁幌? sleep(5);
}
繼續(xù)接著看看異步阻塞的例子,還好有 Swoole 不然這種模式的例子都沒有地方找了,這里感謝一下 Swoole 為 PHP 程序員做的貢獻,讓我們硬氣了一回。構造一個 HTTP 服務并監(jiān)聽了 9501 端口,然后設置了針對 Request 的異步回調(diào)函數(shù),但如果在回調(diào)函數(shù)里面使用了類似 sleep、PDO 等的 PHP 原生函數(shù),就會阻塞整個進程,導致無法處理其他的 Requset 請求。這種情況下的程序性能直接和同步阻塞等同了,所以異步阻塞模式在實際的編程實踐中也不常用,還不如使用同步阻塞模式了。這里提醒一點,在新版的 Swoole 中已經(jīng)可以通過 HOOK 的方式支持 PHP 原生函數(shù)協(xié)程化了,這一點也值得慶幸。
<?php
// 異步阻塞模式
// 創(chuàng)建一個 Swoole 的異步 HTTP 服務器
$http = new Swoole\Http\Server('127.0.0.1', 9501);
// 設置異步回調(diào)函數(shù)
$http->on('request', function ($request, $response) {
// 阻塞了整個進程,使用 PHP 原生的 PDO、Redis 等都會阻塞當前進程
// 結合剛剛的例子,只能干等著,這里你啥也干不了
sleep(5);
$response->end("OK");
});
// 啟動服務器
$http->start();
最后來看看異步非阻塞的例子,這種模式是目前在實踐中性能最好的,和上面例子唯一不同的是在 Request 回調(diào)函數(shù)中使用了協(xié)程類,便不會阻塞整個進程,能夠釋放出 CPU 的控制權去處理其他的請求。當然在新版的 Swoole 中也不一定需要使用協(xié)程類,使用原生的函數(shù)同樣不會阻塞進程了,這一點大大減低了 PHP 程序員編程的心智負擔。
<?php
// 異步非阻塞模式
// 創(chuàng)建一個 Swoole 的異步 HTTP 服務器
$http = new Swoole\Http\Server('127.0.0.1', 9501);
// 設置異步回調(diào)函數(shù)
$http->on('request', function ($request, $response) {
// 不會阻塞整個進程,這里還可以使用類似其他的協(xié)程客戶端
// swoole\Coroutine\MySQL
// swoole\Coroutine\Redis
// 結合剛剛的例子,這里你可以去刷抖音、買82年的可樂
// 也是說你有空去處理其他的請求了,不用這里干等
// 等5秒過后,又可以回來繼續(xù)向下執(zhí)行,接過肉包之后你就可以上班去了,雖然你有百般不舍。
Co::sleep(5);
$response->end("OK");
});
// 啟動服務器
$http->start();
雖然你依然忘不了早餐店美女服務員的容顏,但空空的口袋催促著你趕緊去上班了。看到這里你既欣賞了美女的容顏,同時又把「同步、阻塞、異步、非阻塞」也搞懂了,簡直兩全其美,了解了這些概念對以后學習 Go 語言也大有裨益。但是大家都知道這么一個道理,看懂了并不等于真的懂了,很多人一看就會一做就廢,因此最好自己上手實踐一下,在知中行,在行中知,做到知行合一,就像看美女服務員不是目的而是想要更深入一步交流,就此打住哈哈。在市面上絕大多數(shù)的高性能程序都是異步非阻塞的模式,比如 Nginx、Redis 等,如果大家想寫出高性能的程序最好是優(yōu)先考慮這種模式,因為借鑒才是最快的學習方法。本次分享的內(nèi)容到就此結束了,希望對大家能有所幫助。