Linux C 線程池實現

Linux C 線程池實現

學習網絡編程時,本身動手實現一個Web Server是一個頗有意思的經歷。大多數Web Server都有一個特色:在單位時間內須要處理大量的請求,而且處理這些請求的時間每每還很短。《深刻理解計算機系統》 (CSAPP) 在講解網絡編程時實現了一個經典的Web Server,這個Web Server不只知足了靜態請求,同時還知足了動態請求 (CGI)。雖然這個Web Server可以正常使用,可是仍存在一個明顯的缺陷:它是一個迭代式的Web Server,這意味着在一個請求處理完畢前,不能同時處理另外一個請求,而咱們以前提到Web Server的一個重要特色就是在單位時間內可能會有大量的請求,因此若是投入工業界,這種狀況天然是沒法容忍的。git

多進程 Web Server 模型

解決上面提到的Web Server只能一個接着一個處理請求的第一個方案是:當accept到一個請求時,fork一個子進程去處理這個請求,而主進程仍然在監聽是否有新的鏈接請求。多進程模型在表面上看彷佛解決了問題,可是咱們都知道fork一個進程的開銷是很是大的,基於如下幾個事實。github

  • 從概念上說,能夠將fork認做對父進程程序段、數據段、堆段以及棧段建立拷貝。可是若是真的只是簡單的將父進程虛擬內存頁拷貝到子進程,那就太浪費了。現代UNIX(Linux) 在實現fork時每每會採用兩種技術來避免這種浪費。一是內核將每一進程的代碼段標記爲只讀,從而使得父進程和子進程都沒法修改代碼段。這樣,父進程和子進程能夠共共享同一代碼段。二是對於父進程數據段、堆段和棧段中的各頁,內核採用寫時複製(copy-on-write) 的方式,這麼作的緣由之一是:fork以後經常伴隨着exec,這會用新程序替換進程的代碼段,並從新初始化其數據段、堆段和棧段。可是不管如何,仍存在複製頁表的操做,這也是爲何在UNIX(Linux) 下建立進程要比建立線程開銷大的緣由。編程

  • 併發量一大,此時系統內便會有存在大量的進程,這會致使CPU花費大量的時間在進程調度上,而且進程上下文的切換開銷也很大。網絡

所以,相比於多進程模型,多線程是一個更優的模型:建立線程要快於建立進程,線程間的上下文切換消耗的時間通常也比進程要短。多線程

多線程 Web Server 模型

換用多線程Web Server模型:每accept一個請求,建立一個線程,將請求交由該線程處理。換用多線程模型能夠解決由fork帶來的開銷問題,可是調度問題依然仍是存在的。所以,一個顯而易見的解決辦法是使用線程池,將線程的數量固定下來。基本的實現思路以下。併發

  • 將每一個請求封裝爲一個Job,每一個Job包含線程要執行的方法、傳遞給線程的參數以及用於描述該Job處於Job隊列的位置的參數。學習

  • 線程池維護着一個Job隊列,每一個線程從Job隊列中取下一個Job執行。由於該Job隊列是一個共享資源,所以須要控制線程的同步。優化

  • 初始化線程池時,立刻建立必定數量的線程。此時,這些線程都是阻塞狀態的,由於Job隊列爲空。線程

代碼實現

tinyhttpd是我爲了更有效的學習網絡編程而實現的一個輕量級的Web Server,目前仍有部分問題須要解決以及優化。按照上面的思路,我實現了一個簡單的線程池,並將其引入到tinyhttpd中。具體的代碼實現請參考threadpool.hthreadpool.ccode

剩餘問題

當固定了線程池的線程數量後,仍然存在一個嚴重的問題:實際狀況下,不少鏈接都是長鏈接,這意味着一個線程在處理一個請求時,read到的數據將會是是不連續的。當線程處理完一批數據後,若是繼續read,而下一批數據還未到來時,因爲默認狀況下file descriptorblocking的,所以該線程就會進入阻塞狀態。因此,若是線程池中全部的線程都處於阻塞狀態,此時若是有新的請求到來,那麼是沒法處理的。

解決方案是將file descriptor設置爲non-blocking,利用事件驅動(Event-driven)來處理鏈接。

參考

相關文章
相關標籤/搜索