PHP併發IO編程
併發 IO 問題一直是服務器端編程中的技術難題,從最先的同步阻塞直接 Fork 進程,到 Worker 進程池/線程池,到如今的異步IO、協程。PHP 程序員由於有強大的 LAMP 框架,對這類底層方面的知識知之甚少,本文目的就是詳細介紹 PHP 進行併發 IO 編程的各類嘗試,最後再介紹 Swoole 的使用,深刻淺出全面解析併發 IO 問題。php
多進程/多線程同步阻塞
最先的服務器端程序都是經過多進程、多線程來解決併發IO的問題。進程模型出現的最先,從 Unix 系統誕生就開始有了進程的概念。最先的服務器端程序通常都是 Accept 一個客戶端鏈接就建立一個進程,而後子進程進入循環同步阻塞地與客戶端鏈接進行交互,收發處理數據。
react
多線程模式出現要晚一些,線程與進程相比更輕量,並且線程之間是共享內存堆棧的,因此不一樣的線程之間交互很是容易實現。好比聊天室這樣的程序,客戶端鏈接之間能夠交互,比聊天室中的玩家能夠任意的其餘人發消息。用多線程模式實現很是簡單,線程中能夠直接向某一個客戶端鏈接發送數據。而多進程模式就要用到管道、消息隊列、共享內存,統稱進程間通訊(IPC)複雜的技術才能實現。程序員
代碼實例:
面試
多進程/線程模型的流程是算法
- 建立一個 socket,綁定服務器端口(bind),監聽端口(listen),在PHP中用stream_socket_server一個函數就能完成上面3個步驟,固然也可使用更底層的sockets擴展分別實現。
- 進入while循環,阻塞在accept操做上,等待客戶端鏈接進入。此時程序會進入睡眠狀態,直到有新的客戶端發起connect到服務器,操做系統會喚醒此進程。accept函數返回客戶端鏈接的socket
- 主進程在多進程模型下經過fork(php: pcntl_fork)建立子進程,多線程模型下使用pthread_create(php: new Thread)建立子線程。下文如無特殊聲明將使用進程同時表示進程/線程。
- 子進程建立成功後進入while循環,阻塞在recv(php: fread)調用上,等待客戶端向服務器發送數據。收到數據後服務器程序進行處理而後使用send(php: fwrite)向客戶端發送響應。長鏈接的服務會持續與客戶端交互,而短鏈接服務通常收到響應就會close。
- 當客戶端鏈接關閉時,子進程退出並銷燬全部資源。主進程會回收掉此子進程。
這種模式最大的問題是,進程/線程建立和銷燬的開銷很大。因此上面的模式沒辦法應用於很是繁忙的服務器程序。對應的改進版解決了此問題,這就是經典的 Leader-Follower 模型。編程
代碼實例:
數組
它的特色是程序啓動後就會建立N個進程。每一個子進程進入 Accept,等待新的鏈接進入。當客戶端鏈接到服務器時,其中一個子進程會被喚醒,開始處理客戶端請求,而且再也不接受新的TCP鏈接。當此鏈接關閉時,子進程會釋放,從新進入 Accept ,參與處理新的鏈接。緩存
這個模型的優點是徹底能夠複用進程,沒有額外消耗,性能很是好。不少常見的服務器程序都是基於此模型的,好比 Apache 、PHP-FPM。服務器
- 這種模型嚴重依賴進程的數量解決併發問題,一個客戶端鏈接就須要佔用一個進程,工做進程的數量有多少,併發處理能力就有多少。操做系統能夠建立的進程數量是有限的。
- 啓動大量進程會帶來額外的進程調度消耗。數百個進程時可能進程上下文切換調度消耗佔CPU不到1%能夠忽略不計,若是啓動數千甚至數萬個進程,消耗就會直線上升。調度消耗可能佔到 CPU 的百分之幾十甚至 100%。
另外有一些場景多進程模型沒法解決,好比即時聊天程序(IM),一臺服務器要同時維持上萬甚至幾十萬上百萬的鏈接(經典的C10K問題),多進程模型就力不從心了。
還有一種場景也是多進程模型的軟肋。一般Web服務器啓動100個進程,若是一個請求消耗100ms,100個進程能夠提供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次是命中的,剩下的99萬9999次都是無效的,白白浪費了CPU資源。
直到Linux 2.6內核提供了新的epoll系統調用,能夠維持無限數量的鏈接,並且無需輪詢,這才真正解決了 C10K 問題。如今各類高併發異步IO的服務器程序都是基於epoll實現的,好比Nginx、Node.js、Erlang、Golang。像 Node.js 這樣單進程單線程的程序,均可以維持超過1百萬TCP鏈接,所有歸功於epoll技術。
IO複用異步非阻塞程序使用經典的Reactor模型,Reactor顧名思義就是反應堆的意思,它自己不處理任何數據收發。只是能夠監視一個socket句柄的事件變化。

Reactor有4個核心的操做:
- add添加socket監聽到reactor,能夠是listen socket也可使客戶端socket,也能夠是管道、eventfd、信號等
- set修改事件監聽,能夠設置監聽的類型,如可讀、可寫。可讀很好理解,對於listen socket就是有新客戶端鏈接到來了須要accept。對於客戶端鏈接就是收到數據,須要recv。可寫事件比較難理解一些。一個SOCKET是有緩存區的,若是要向客戶端鏈接發送2M的數據,一次性是發不出去的,操做系統默認TCP緩存區只有256K。一次性只能發256K,緩存區滿了以後send就會返回EAGAIN錯誤。這時候就要監聽可寫事件,在純異步的編程中,必須去監聽可寫才能保證send操做是徹底非阻塞的。
- del從reactor中移除,再也不監聽事件
- callback就是事件發生後對應的處理邏輯,通常在add/set時制定。C語言用函數指針實現,JS能夠用匿名函數,PHP能夠用匿名函數、對象方法數組、字符串函數名。
Reactor只是一個事件發生器,實際對socket句柄的操做,如connect/accept、send/recv、close是在callback中完成的。具體編碼可參考下面的僞代碼:

Reactor模型還能夠與多進程、多線程結合起來用,既實現異步非阻塞IO,又利用到多核。目前流行的異步服務器程序都是這樣的方式:如
- Nginx:多進程Reactor
- Nginx+Lua:多進程Reactor+協程
- Golang:單線程Reactor+多線程協程
- Swoole:多線程Reactor+多進程Worker
協程是什麼
協程從底層技術角度看實際上仍是異步IO Reactor模型,應用層自行實現了任務調度,藉助Reactor切換各個當前執行的用戶態線程,但用戶代碼中徹底感知不到Reactor的存在。
PHP併發IO編程實踐
PHP相關擴展
- Stream:PHP內核提供的socket封裝
- Sockets:對底層Socket API的封裝
- Libevent:對libevent庫的封裝
- Event:基於Libevent更高級的封裝,提供了面向對象接口、定時器、信號處理的支持
- Pcntl/Posix:多進程、信號、進程管理的支持
- Pthread:多線程、線程管理、鎖的支持
- PHP還有共享內存、信號量、消息隊列的相關擴展
- PECL:PHP的擴展庫,包括系統底層、數據分析、算法、驅動、科學計算、圖形等都有。若是PHP標準庫中沒有找到,能夠在PECL尋找想要的功能。
PHP語言的優劣勢
PHP的優勢:
- 第一個是簡單,PHP比其餘任何的語言都要簡單,入門的話PHP真的是能夠一週就入門。C++有一本書叫作《21天深刻學習C++》,其實21天根本不可能學會,甚至能夠說C++沒有3-5年不可能深刻掌握。可是PHP絕對能夠7天入門。因此PHP程序員的數量很是多,招聘比其餘語言更容易。
- PHP的功能很是強大,由於PHP官方的標準庫和擴展庫裏提供了作服務器編程能用到的99%的東西。PHP的PECL擴展庫裏你想要的任何的功能。
- 另外PHP有超過20年的歷史,生態圈是很是大的,在Github能夠找到不少代碼。
PHP的缺點:
- 性能比較差,由於畢竟是動態腳本,不適合作密集運算,若是一樣的 PHP 程序使用 C/C++ 來寫,PHP 版本要比它差一百倍。
- 函數命名規範差,這一點你們都是瞭解的,PHP更講究實用性,沒有一些規範。一些函數的命名是很混亂的,因此每次你必須去翻PHP的手冊。
- 提供的數據結構和函數的接口粒度比較粗。PHP只有一個Array數據結構,底層基於HashTable。PHP的Array集合了Map,Set,Vector,Queue,Stack,Heap等數據結構的功能。另外PHP有一個SPL提供了其餘數據結構的類封裝。
- PHP更適合偏實際應用層面的程序,業務開發、快速實現的利器
- PHP不適合開發底層軟件
- 使用C/C++、JAVA、Golang等靜態編譯語言做爲PHP的補充,動靜結合
- 藉助IDE工具實現自動補全、語法提示