首頁  >  文章  >  後端開發  >  PHP並發IO編程詳解

PHP並發IO編程詳解

小云云
小云云原創
2018-03-22 09:15:131864瀏覽

並發 IO 問題一直是伺服器端程式設計中的技術難題,從最早的同步阻斷直接Fork 進程,到Worker 進程池/線程池,到現在的非同步IO、協程。 PHP 程式設計師因為有強大的 LAMP 框架,對這類底層的知識知之甚少,本文目的就是詳細介紹 PHP 進行並發 IO 編程的各種嘗試,最後再介紹 Swoole 的使用,深入淺出全面解析並發 IO 問題。

多重行程/#多執行緒同步阻塞

最早的伺服器端程式都是透過多進程、多執行緒來解決並發IO的問題。進程模型出現的最早,從 Unix 系統誕生就開始有了進程的概念。 最早的伺服器端程式一般都是 Accept 一個客戶端連線就建立一個行程,然後子行程進入循環同步阻塞地與客戶端連線進行交互,收發處理資料。

多執行緒模式出現要晚一些,執行緒與進程相比更輕量,而且執行緒之間是共享記憶體堆疊的,所以不同的線程之間交互非常容易實現。例如聊天室這樣的程序,客戶端連線之間可以交互,比聊天室中的玩家可以任意的其他人傳訊息。用多執行緒模式實作非常簡單,在線程中可以直接向某一個客戶端連線發送資料。而多進程模式就要用到管道、訊息佇列、共享內存,統稱進程間通訊(IPC)複雜的技術才能實現。

程式碼實例:

#多重行程##/執行緒模型的流程是

  1. 建立一個 socket#,綁定伺服器連接埠(bind),監聽埠(listen#),在##PHP中用stream_socket_server一個函數就能完成上面##3

  2. 個步驟,當然也可以使用更底層的sockets#擴展分別實作。 進入while」循環,阻塞在accept操作上,等待客戶端連線進入。此時程式會進入睡眠狀態,直到有新的客戶端發起connect

  3. 到伺服器,作業系統會喚醒此程序。
  4. accept函數傳回客戶端連線的socket##主程序在多進程模型下透過fork#(php: pcntl_fork)建立子進程,多執行緒模型下使用pthread_create##(

    #php : new Thread#########)建立子執行緒。下文如無特殊宣告將使用行程同時表示進程#########/##########執行緒。 ######
  5. 子程序建立成功後進入while循環,阻塞在# recvphp: fread)呼叫上,等待客戶端傳送資料到伺服器。收到資料後伺服器程式進行處理然後使用sendphp: fwrite)向客戶端發送回應。長連線的服務會持續與客戶端交互,而短連線服務一般收到回應就會close##。

  6. 當客戶端連線關閉時,子程序退出並銷毀所有資源。主進程會回收掉此子進程。

這種模式最大的問題是,進程/執行緒建立和銷毀的開銷很大。所以上面的模式沒辦法應用在非常繁忙的伺服器程式。對應的改良版解決了這個問題,也就是經典的 Leader-Follower 模式。

程式碼實例:

#它的特點是程式啟動後就會建立#N個行程。每個子程序進入 Accept,等待新的連線進入。當客戶端連接到伺服器時,其中一個子程序會被喚醒,開始處理客戶端請求,並且不再接受新的TCP##連線。當此連線關閉時,子程序會釋放,重新進入 Accept ,參與處理新的連線。

這個模型的優點是完全可以重複使用進程,沒有額外消耗,效能非常好。許多常見的伺服器程式都是基於此模型的,例如 ##Apache ##PHP-FPM

  1. ##。 多行程模型也有一些缺點。 這種模型嚴重依賴進程的數量來解決並發問題,一個客戶端連線就需要佔用一個進程,工作進程的數量有多少,並發處理能力就有多少。作業系統可以建立的進程數量是有限的。 啟動大量進程會帶來額外的進程調度消耗。數百個行程時可能進程上下文切換調度消耗佔CPU#不到

  2. 1%

#可以忽略不接,如果啟動數千甚至數萬個進程,消耗就會直線上升。調度消耗可能佔 CPU #的百分之幾十甚至 ##100% #。

#########另外有一些場景多進程模型無法解決,例如即時聊天程式(#########IM####### ##),一台伺服器要同時維持上萬甚至幾十萬上百萬的連線(經典的#########C10K#########問題),多進程模型就力不從心了。 ######

還有一種場景也是多進程模型的軟肋。通常Web伺服器啟動#100個進程,如果一個請求消耗100ms100 1000qps,這樣的處理能力還是不錯的。但如果請求內要呼叫外網Http接口,像QQ、微博登錄,耗時會很長,一個請求需要10s。那一個行程1秒只能處理#0.1個請求,

100個行程只能達到#10qps,這樣的處理能力就太差了。 有沒有一種技術可以在一個行程內處理所有並發

###IO#########呢?答案是有,這就是#########IO#########重用技術。 ######

IO復用/事件循環 #/異步非阻塞

其實IO復用的歷史和多進程一樣長,Linux很早就提供了 select 系統調用,可以在一個進程內維持1024個連接。後來又加入了poll系統調用,poll做了一些改進,解決了 1024 限制的問題,可以維持任意數量的連結。但select/poll還有一個問題就是,它需要循環偵測連線是否有事件。這樣問題就來了,如果伺服器有100萬個連接,在某一時間只有一個連接向伺服器發送了數據,select/poll需要做循環100萬次,只有1次是命中的,剩下的999999 次都是無效的,白白浪費了CPU資源

直到Linux 2.6#核心提供了新的##epoll系統調用,可以維持無限數量的連接,而且無需輪詢,這才真正解決了 #C10K ##問題問題。現在各種高並發非同步IO的伺服器程式都是基於epoll實現的,例如Nginx#Node.js##、ErlangGolang。像 Node.js 這樣單一行程單執行緒的程序,都可以維持超過##1 百萬TCP連接,全部歸功於epoll## 技術。 IO

複用非同步非阻塞程式使用經典的Reactor 模型,Reactor#顧名思義就是反應器的意思,它本身不處理任何資料收發。只是可以監視一個socket句柄的事件變化。

Reactor

#有##4個核心的操作:#

  1. add新增#socket##監聽到 reactor,可以是listen socket也可以讓客戶端socketeventfd 、訊號等

  2. #set修改事件監聽,可以設定監聽的類型,如可讀、可寫入。可讀很好理解,對於listen socket就是有新客戶端連線到來了需要accept。對於客戶端連線就是收到數據,需要recv。可寫事件比較難理解一些。一個SOCKET是有快取區的,如果要連接到客戶端2M的數據,一次性是發不出去的,作業系統預設##TCP#快取區只有256K。一次只能發256K,就會回傳EAGAIN錯誤。這時候就要監聽可寫事件,在純粹非同步的程式設計中,必須去監聽可寫才能保證send操作是完全非阻塞的。 del

  3. reactor

    ## #中移除,不再監聽事件######
  4. callback就是事件發生後對應的處理邏輯,一般在##add/set時制定。 C語言用函數指標實現,#JS可以用匿名函數,PHP可以用匿名函數、物件方法陣列、字串函數名。


Reactor只是一個事件產生器,實際對socket句柄的操作,如#connect/accept##、#send/recvclose是在callback 中完成的。具體編碼可參考下面的偽代碼:

#Reactor模型還可以與多進程、多執行緒結合起來用,既實現異步非阻塞IO

##IO
  • ##IO#目前流行的非同步伺服器程式都是這樣的方式:如

  • #Nginx:多進程 Reactor

  • #Nginx+Lua
  • ##:多進程Reactor+協程

    #Golang#########:單執行緒#########Reactor+#########多執行緒協程######
  • Swoole:多執行緒Reactor+多進程Worker

# 協程是什麼

協程從底層技術角度來看其實還是非同步IO Reactor模型,應用層自行實現了任務調度,借助Reactor切換各個目前執行的用戶態線程,但用戶程式碼中完全感知不到Reactor的存在。


PHP並發IO程式設計實作

PHP相關擴充功能

  • StreamPHP核心提供的 #socket封裝

  • #Sockets##:對底層#Socket API的封裝

  • #Libevent:對libevent#庫的封裝

  • Event:基於Libevent更進階的封裝,提供了物件導向介面、計時器、訊號處理的支援

  • Pcntl/Posix:多重進程、訊號、行程管理的支援

  • ############################### #Pthread#########:多執行緒、執行緒管理、鎖定的支援#####################PHP####### ##還有共享記憶體、訊號量、訊息佇列的相關擴充######
  • PECL#PHP的擴充庫,包括系統底層、資料分析、演算法、驅動、科學計算、圖形等都有。如果PHP標準庫中找不到,可以在PECL尋找想要的功能。

PHP語言的優缺點

## PHP的優點:

  1. #第一個是簡單,##PHP 比其他任何的語言都要簡單,入門的話PHP真的可以一週就入門。 C++有一本書叫做《#21天深入學習C++》,其實21天根本不可能學會,甚至可以說C++沒有3-5年不可能深入掌握。但PHP絕對可以#7天入門。所以PHP程式設計師的數量非常多,招募比其他語言更容易。

  2. PHP的功能非常強大,因為PHP官方的標準函式庫和擴充函式庫裡提供了做伺服器程式設計能用到的99%的東西。 PHP#PECL的功能庫中你想要的任何功能。

另外PHP有超過20 年的歷史,生態圈是非常大的,在Github可以找到很多程式碼。

PHP的缺點:

  1. 效能比較差,因為畢竟是動態腳本,不適合做密集運算,如果同樣的 #PHP 程式讓##用 C/C++ 來寫,#PHP ##版本要比它差一百倍。

  2. 函數命名規格差,這一點大家都是了解的,

    ##PHP更講究實用性,沒有一些規範。有些函數的命名是很混亂的,所以每次你必須去翻PHP#的手冊。

  3. 提供的資料結構和函數的介面粒度比較粗。 PHP只有一個#Array資料結構,底層基於HashTablePHPArray集合了 MapSetVectorQueueStack# Heap

    等資料結構的功能。另外

PHP有一個#SPL

  1. 提供了其他資料結構的類別封裝。

    所以
  2. PHP

  3. PHP

    更適合偏實際應用層面的程序,業務開發、快速實現的利器PHP不適合開發底層軟體使用C/C++

  4. JAVA

    Golang等靜態編譯語言作為

    PHP
的補充,動靜結合


### ######利用######IDE######工具實作自動補全、語法提示 #####################

PHPSwoole擴充

#基於上面的擴充功能使用純##PHPHP 就可以完全實現非同步網路伺服器和客戶端程式。但是想實現一個類似於多IO線程,還是有很多繁瑣的程式設計工作要做,包括如何來管理連接,如何來保證資料的收發原子性,網路協定的處理。另外PHP程式碼在協定處理部分效能是比較差的,所以我啟動了一個新的開源專案Swoole ,使用C語言和PHP結合來完成了這項工作。靈活多變的業務模組使用PHP開發效率高,基礎的底層和協定處理部分用#C 語言實現,保證了高性能。它以擴展的方式加載到了PHP中,提供了一個完整的網路通訊的框架,然後PHP的程式碼去寫一些業務。它的模型是基於多執行緒Reactor+多重行程Worker,既支持全異步,也支援半異步半同步。

Swoole的一些特點:

  • ## Accept線程,解決Accept效能瓶頸和驚群問題

  • IO線程,可以更好地利用多核心

  • 提供了全異步和半同步半非同步
  • 2#種模式

  • 處理高並發
  • IO的部分用非同步模式

  • 複雜的業務邏輯部分用同步模式
  • 底層支援了遍歷所有連接、互發資料、自動合併分割資料包、資料發送原子性。

Swoole的行程/##執行緒模型:

Swoole#程式的執行流程:

##使用

PHP+Swoole

擴展實現非同步通訊程式設計實例程式碼在

https://github.com/swoole/swoole-src 

首頁查看。

TCP

伺服器與客戶端

非同步

TCP伺服器:

 

在這裡

new swoole_server對象,然後參數傳入監聽的HOSTPORT#。 #3個回呼函數,分別是onConnect有新的連線進入、onReceive收到了某一個客戶端的資料、onClose某個客戶端關閉了連接。最後呼叫start

###啟動伺服器程式。 #########swoole#########底層會根據目前機器有多少#########CPU########核數,啟動對應數量的#########Reactor##########線程和##########Worker########程式。 ######

非同步客戶端:

#客戶端的使用方法和伺服器類似只是回呼事件有4個,onConnect成功連接到伺服器,這時可以去發送資料到伺服器. onError連線伺服器失敗。 onReceive伺服器向客戶端連線發送了資料。 onClose連線關閉。

設定完事件回呼後,發起connect到伺服器,參數是伺服器的IP,PORT和超時時間。

 

同步用戶端:

##同步客戶端不需要設定任何事件回調,它沒有Reactor#監聽,是阻塞串列的。等待IO完成才會進入下一步。

非同步任務:

#

非同步任務功能用於在一個純粹非同步的Server程式中去執行一個耗時的或是阻塞的函數。底層實作使用進程池,任務完成後會觸發onFinish,程式中可以得到任務處理的結果。例如一個IM需要廣播,如果直接在非同步程式碼中廣播可能會影響其他事件的處理。另外檔案讀寫也可以使用非同步任務實現,因為檔案句柄沒辦法像socket一樣使用Reactor監聽。因為檔案句柄總是可讀的,直接讀取檔案可能會使伺服器程式阻塞,使用非同步任務是非常好的選擇。

非同步毫秒定時器

2個介面實作了類似JSsetIntervalsetTimeout函數功能,可以設定在n毫秒間隔實作一個函數或 n毫秒後執行一個函數。

非同步#MySQL客戶端

swoole也提供一個內建連線池的MySQL##異步客戶端,可以設定最大使用MySQL#連接數。並發SQL請求可以重複使用這些連接,而不是重複創建,這樣可以保護MySQL避免連線資源被耗盡。

非同步#Redis客戶端

異步的##Web#程式

#程式的邏輯是從

##Redis中讀取一個數據,然後顯示HTML#頁面。使用ab壓測效能如下:

同樣的邏輯在

php-fpm下的效能測試結果如下:

WebSocket#程式

#

swoole內建了websocket伺服器,可以基於此實作Web頁面主動推送的功能,例如WebIM。有一個開源專案可以作為參考。 https://github.com/matyhtf/php-webim

PHP+Swoole協程

非同步程式設計一般使用回呼方式,如果遇到非常複雜的邏輯,可能會層層嵌套回呼函數。協程就可以解決此問題,可以順序編寫程式碼,但執行時間是非同步非阻塞的。騰訊的工程師基於Swoole擴充功能和##PHP5.5 Yield/Generator語法實作類似#Golang的協程,專案名稱為TSFTencent Server Framework),開源專案位址:https://github.com/tencent-php/tsf。目前在騰訊公司的企業QQQQ公眾號專案以及車輪忽略的查違項目有大規模應用 。

TSF使用也非常簡單,下面呼叫了3#個IO操作,完全是串列的寫法。但實際上是異步非阻塞執行的。 TSF底層調度器接管了程式的執行,在對應的IO完成後才會向下繼續執行。

#

以上是PHP並發IO編程詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn