一直讓 PHP 程式設計師懵逼的同步阻塞非同步非阻塞,終於搞明白了

Yxh_blogs發表於2024-08-05

大家好,我是碼農先森。

經常聽到身邊寫 Java、Go 的朋友提到程式非同步、非阻塞、執行緒、協程,讓系統效能提高到百萬、千萬併發,使我甚是驚訝屬實羨慕。對於常年寫 PHP 的我來說,最初聽到這幾個詞時,腦袋一直處於蒙圈狀態,回過頭來看著自己手上同步阻塞的 PHP 程式碼,心想著「非同步、非阻塞、執行緒、協程」到底是個什麼東東,這麼厲害嘛。其實 PHP 中也有執行緒、協程,但在日常的程式設計中幾乎不會使用,原因是 PHP-FPM 多程序模式下並不支援執行緒、協程,使用 PHP 程式設計的程式設計師絕大多數都離不開 PHP-FPM 。這也就導致了 PHP 程式設計師對那些概念沒有接觸,那就更別提理解了,因此為了廣大的 PHP 程式設計師同胞們能夠和 Java、Go 的程式設計師對上話,特地對「同步、阻塞、非同步、非阻塞」這幾個概念進行了深度的分析,爭取把 PHP 程式設計師的腰桿挺直溜。

按照慣例先上八股文這道菜:

  • 同步阻塞:當一個操作被呼叫時,呼叫者將被阻塞,直到這個操作完成並返回結果。在此期間,呼叫者無法進行其他任務。
  • 非同步阻塞:當一個操作被呼叫時,呼叫者不會被阻塞,而是可以繼續執行其他任務。然而,它仍然需要等待被呼叫的操作完成,並在操作完成後處理其結果。這個等待過程可能是阻塞的。
  • 同步非阻塞:呼叫者發起一個操作後,不會被阻塞並可以繼續執行其他任務。雖然呼叫者可以立即獲得控制權,但它仍然需要等待操作完成才能處理結果。在等待的過程中,呼叫者可以主動輪詢或者不斷嘗試獲取操作結果,以避免長時間的阻塞。
  • 非同步非阻塞:呼叫者發起一個操作後,不會被阻塞並可以繼續執行其他任務。同時,呼叫者也不需要等待操作完成來處理結果。相反,呼叫者可以註冊一個回撥函式或者使用類似事件驅動的機制,當操作完成後被自動觸發回撥函式來處理結果。

基礎知識紮實的朋友看這個八股文就足以解惑了,不過看得懂八股文的畢竟是少數英俊帥氣人,你說氣不氣人集顏值與才華於一體,別看說的就是各位看官「哈哈」。言歸正傳,那看不懂八股文的怎麼搞?別急,且聽我結合生活中的例子娓娓道來。

你每天上班匆匆路過的早餐店,今天額外的多人,你湊近一看原來是來了位身材高挑楚楚動人的美女服務員,結果你按耐不住心中的激動,今天高低得買兩個饅頭外加一杯豆漿,由於買的人太多,蒸好的饅頭早已賣完,這時你只能等正在蒸的,期間你什麼也幹不了只能眼勾勾的乾等著,那麼這時的你是同步阻塞的。

由於來買早餐的人越來越多,離上班的時間也越來越近,你開始了騷動,每隔幾分鐘就問美女服務員饅頭蒸好了沒?此時的你不再幹等,而是開始刷刷抖音看看工作群,因為你已經付錢了所以還是得等饅頭,由於美女服務員太忙了沒空主動告訴你,需要你自己不斷地問,那麼這時的你是同步非阻塞的。

過了高峰期人變少了,視野更廣闊了,你看美女服務員更清楚了,結果你又開始眼勾勾的乾等著,抖音也不刷了工作群的訊息也不顧了。由於美女服務員不忙了,開始主動叫那位身穿格子衫背雙肩包帥哥,饅頭蒸好了,這時的你甩了甩頭上的劉海,接過了美女服務員手中的饅頭會心一笑,順便還加了對方的微信,那麼此時的你是非同步阻塞的。

隔天你為了再睹芳容,又來到了這家早餐店,一向摳門的你甩手就點了兩個肉包。這時美女服務員迎面笑臉告知你肉包還需耐心等待哦,蒸好了會微信通知你。在炎炎的夏日裡你路上走的太匆忙,此時的你口渴難耐,就去隔壁小賣部買了瓶82年的可樂,還坐著吹了會空調。隨著微信的一聲叮咚,你起身去早餐店,接過了美女服務員手中的肉包,那麼此時的你是非同步非阻塞的。

有了美女服務員的投餵,你工作的幹勁都十足了,同時應該也把「同步、阻塞、非同步、非阻塞」這幾個概念搞懂了吧。其實這裡的同步非同步和阻塞非阻塞,容易搞混淆就像你看美女服務員容易丟魂一樣,在這個例子中同步非同步需要關注的是「美女服務員是否會主動的通知你」,主動通知你那麼對你來說就是非同步的,需要你去詢問那麼對你來說就是同步的。阻塞非阻塞需要關注的點是「你是否是眼勾勾的乾等著」,如果你只能乾等那就是阻塞的,如果你還能幹點其他的事情比如刷抖音、買82年的可樂,那麼就是非阻塞的。

美女也看了道理也懂了,有的朋友們又要產生新的疑問了,那在程式中怎麼體現、怎麼用「同步、阻塞、非同步、非阻塞」呢?那我們就開始上程式碼,畢竟看美女服務員的目的也是為了能夠深入交往嘛,也就等同於實踐上手了,你細品是不是這個理。

開整!

我們先來看同步阻塞的例子,使用 socket_create、socket_bind、socket_listen 函式建立繫結並監聽了 8080 埠,然後一直阻塞在 socket_accept 函式上,直到有客戶端連線的到來。傳統的 PHP-FPM 就是同步阻塞的模式,不過 PHP-FPM 多程序模型,在接收到客戶端連線 $client 後就交給由子程序進行後續的處理了,在這個例子只舉例了單程序的模式。

<?php

// 同步阻塞模式

// 建立一個監聽 Socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

// 繫結 8080 埠
socket_bind($socket, '0.0.0.0', 8080);

// 開始監聽
socket_listen($socket);

while(true){
    // 會阻塞在這裡,一直等著客戶端來連線
    // 結合剛剛的例子可以理解為,你一直在這裡眼勾勾的乾等饅頭,啥也幹不了
    $client = socket_accept($socket);
    if($client){
        echo "客官來了" . PHP_EOL;
    }
}

再來看看同步非阻塞的例子,同樣也是監聽了 8080 埠,不同的是將套接字 $socket 設定成了非阻塞模式。那麼這種情況下將不會一直阻塞在 socket_accept 函式上,會繼續往下執行,如果沒有寫其他的邏輯,就會出現放空炮的現象。這種模式在實際的程式設計中基本上不會採用,會把系統榨乾,這一點值得注意一下,誰寫了這樣的程式碼就要拉出去罰站了。

<?php

// 同步非阻塞模式

// 建立一個監聽 Socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

// 繫結 8080 埠
socket_bind($socket, '0.0.0.0', 8080);

// 開始監聽
socket_listen($socket);

// 這裡設定成非阻塞
socket_set_nonblock($socket);

while(true){
    // 不會阻塞在這裡
    $client = socket_accept($socket);
    if($client){
        echo "客官來了" . PHP_EOL;
    }

    // 會繼續往下執行
    // 結合剛剛的例子,可以在這裡刷刷抖音、看看工作群訊息等
    // ...

    // 如果你上面沒有寫任何的邏輯,這些最好 sleep 一下
    // 不然 CPU 就會被榨乾了,也就是說不要一直眼勾勾的盯著美女服務員會被吸乾
    // 要適當的休息一下
    sleep(5);
}

繼續接著看看非同步阻塞的例子,還好有 Swoole 不然這種模式的例子都沒有地方找了,這裡感謝一下 Swoole 為 PHP 程式設計師做的貢獻,讓我們硬氣了一回。構造一個 HTTP 服務並監聽了 9501 埠,然後設定了針對 Request 的非同步回撥函式,但如果在回撥函式里面使用了類似 sleep、PDO 等的 PHP 原生函式,就會阻塞整個程序,導致無法處理其他的 Requset 請求。這種情況下的程式效能直接和同步阻塞等同了,所以非同步阻塞模式在實際的程式設計實踐中也不常用,還不如使用同步阻塞模式了。這裡提醒一點,在新版的 Swoole 中已經可以透過 HOOK 的方式支援 PHP 原生函式協程化了,這一點也值得慶幸。

<?php

// 非同步阻塞模式

// 建立一個 Swoole 的非同步 HTTP 伺服器
$http = new Swoole\Http\Server('127.0.0.1', 9501);

// 設定非同步回撥函式
$http->on('request', function ($request, $response) {
    // 阻塞了整個程序,使用 PHP 原生的 PDO、Redis 等都會阻塞當前程序
    // 結合剛剛的例子,只能乾等著,這裡你啥也幹不了
    sleep(5);

    $response->end("OK");
});

// 啟動伺服器
$http->start();

最後來看看非同步非阻塞的例子,這種模式是目前在實踐中效能最好的,和上面例子唯一不同的是在 Request 回撥函式中使用了協程類,便不會阻塞整個程序,能夠釋放出 CPU 的控制權去處理其他的請求。當然在新版的 Swoole 中也不一定需要使用協程類,使用原生的函式同樣不會阻塞程序了,這一點大大減低了 PHP 程式設計師程式設計的心智負擔。

<?php

// 非同步非阻塞模式

// 建立一個 Swoole 的非同步 HTTP 伺服器
$http = new Swoole\Http\Server('127.0.0.1', 9501);

// 設定非同步回撥函式
$http->on('request', function ($request, $response) {
    // 不會阻塞整個程序,這裡還可以使用類似其他的協程客戶端
    // swoole\Coroutine\MySQL
    // swoole\Coroutine\Redis
    // 結合剛剛的例子,這裡你可以去刷抖音、買82年的可樂
    // 也是說你有空去處理其他的請求了,不用這裡乾等
    // 等5秒過後,又可以回來繼續向下執行,接過肉包之後你就可以上班去了,雖然你有百般不捨。
    Co::sleep(5);

    $response->end("OK");
});

// 啟動伺服器
$http->start();

雖然你依然忘不了早餐店美女服務員的容顏,但空空的口袋催促著你趕緊去上班了。看到這裡你既欣賞了美女的容顏,同時又把「同步、阻塞、非同步、非阻塞」也搞懂了,簡直兩全其美,瞭解了這些概念對以後學習 Go 語言也大有裨益。但是大家都知道這麼一個道理,看懂了並不等於真的懂了,很多人一看就會一做就廢,因此最好自己上手實踐一下,在知中行,在行中知,做到知行合一,就像看美女服務員不是目的而是想要更深入一步交流,就此打住哈哈。在市面上絕大多數的高效能程式都是非同步非阻塞的模式,比如 Nginx、Redis 等,如果大家想寫出高效能的程式最好是優先考慮這種模式,因為借鑑才是最快的學習方法。本次分享的內容到就此結束了,希望對大家能有所幫助。

感謝大家閱讀,個人觀點僅供參考,歡迎在評論區發表不同觀點。


歡迎關注、分享、點贊、收藏、在看,我是微信公眾號「碼農先森」作者。

相關文章