伺服器并發模型通常可分為單線程和多線程模型,這裡的線程通常是指“i/o線程”,即負責i/o操作,協調配置設定任務的“管理線程”,而實際的請求和任務通常交由所謂“工作者線程”處理。通常多線程模型下,每個線程既是i/o線程又是工作者線程。是以這裡讨論的是,單i/o線程+多工作者線程的模型,這也是最常用的一種伺服器并發模型。我所在的項目中的server代碼中,這種模型随處可見。它還有個名字,叫“半同步/半異步“模型,同時,這種模型也是生産者/消費者(尤其是多消費者)模型的一種表現。
這種架構主要是基于i/o多路複用的思想(主要是epoll,select/poll已過時),通過單線程i/o多路複用,可以達到高效并發,同時避免了多線程i/o來回切換的各種開銷,思路清晰,易于管理,而基于線程池的多工作者線程,又可以充分發揮和利用多線程的優勢,利用線程池,進一步提高資源複用性和避免産生過多線程。
瓶頸在于io密集度。
線程池你開10個線程當然可以一上來全部accept阻塞住,這樣用戶端一連上來便會自動激活一個線程去處理,但是設想一下,如果10個線程全部用掉了,第11個用戶端就會發生丢棄。這樣為了實作”高并發“你得不斷加大線程池的數量。這樣會帶來嚴重的記憶體占用和線程切換的時延問題。
于是前置事件輪詢設施的方案就應運而生了,
主線程輪詢負責io,作業交給線程池。
在高并發下,10w個用戶端上來,就主線程負責accept,放到隊列中,不至于發生沒有及時握手而丢棄掉連接配接的情況發生,而作業線程從隊列中認領作業,做完回複主線程,主線程負責write。這樣可以用極少的系統資源處理大數量連接配接。
在低并發下,比如2個用戶端上來,也不會出現100個線程hold住在那進而發生系統資源浪費的情況。
正确實作基本線程池模型的核心:
主線程負責所有的 i/o 操作,收齊一個請求所有資料之後如果有必要,交給工作線程進行處理 。處理完成之後,把需要寫回的資料還給主線程去做寫回 / 嘗試寫回資料直到阻塞,然後交回主線程繼續。
這裡「如果有必要」的意思是:經過測量,确認這個處理過程中所消耗的 cpu 時間(不包括任何 i/o 等待,或者相關的 i/o 等待操作無法用 epoll 接管)相當顯著。如果這個處理過程(不包含可接管的 i/o 操作)不顯著,則可以直接放在主線程裡解決。
這個「必要」與否的前提不過三個詞:假設,分析,測量。
是以,一個正确實作的線程池環境鐘,用 epoll + non-blocking i/o 代替 select + blocking i/o 的好處是,處理大量 socket 的時候,前者效率比後者高,因為前者不需要每次被喚醒之後重新檢查所有 fd 判斷哪個 fd 的狀态改變可以進行讀寫了。
實作單i/o線程的epoll模型是本架構的第一個技術要點,主要思想如下:
單線程建立epoll并等待,有i/o請求(socket)到達時,将其加入epoll并從線程池中取一個空閑工作者線程,将實際的業務交由工作者線程處理。
剛學線程池,若有誤請大家指出(可聯系我,下有郵箱):
伺服器代碼:
lock.h
threadpool.h,還沒有實作動态增加功能,以後待更新...
epollserver.h
server.cpp伺服器的主函數
用戶端程式:
github:https://github.com/tianzengblog/websserver