服務器模型——從單線程阻塞到多線程非阻塞(中)

前言的前言

服務器模型涉及到線程模式和IO模式,搞清楚這些就能針對各類場景有的放矢。該系列分紅三部分:服務器

  • 單線程/多線程阻塞I/O模型
  • 單線程非阻塞I/O模型
  • 多線程非阻塞I/O模型,Reactor及其改進

前言

這裏探討的服務器模型主要指的是服務器端對I/O的處理模型。從不一樣維度能夠有不一樣的分類,這裏從I/O的阻塞與非阻塞、I/O處理的單線程與多線程角度探討服務器模型。多線程

對於I/O,能夠分紅阻塞I/O與非阻塞I/O兩大類型。阻塞I/O在作I/O讀寫操做時會使當前線程進入阻塞狀態,而非阻塞I/O則不進入阻塞狀態。併發

對於線程,單線程狀況下由一條線程負責全部客戶端鏈接的I/O操做,而多線程狀況下則由若干線程共同處理全部客戶端鏈接的I/O操做。機器學習

單線程非阻塞I/O模型

多線程阻塞I/O模型經過引入多線程確實提升了服務器端的併發處理能力,但每一個鏈接都須要一個線程負責I/O操做。當鏈接數量較多時可能致使機器線程數量太多,而這些線程大多數時間卻處於等待狀態,形成極大的資源浪費。鑑於多線程阻塞I/O模型的缺點,有沒有可能用一個線程就能夠維護多個客戶端鏈接而且不會阻塞在讀寫操做呢?下面介紹單線程非阻塞I/O模型。socket

單線程非阻塞I/O模型最重要的一個特色是,在調用讀取或寫入接口後當即返回,而不會進入阻塞狀態。在探討單線程非阻塞I/O模型前必需要先了解非阻塞狀況下套接字事件的檢測機制,由於對於單線程非阻塞模型最重要的事情是檢測哪些鏈接有感興趣的事件發生。通常會有以下三種檢測方式。分佈式

應用程序遍歷套接字的事件檢測

當多個客戶端向服務器請求時,服務器端會保存一個套接字鏈接列表中,應用層線程對套接字列表輪詢嘗試讀取或寫入。對於讀取操做,若是成功讀取到若干數據,則對讀取到的數據進行處理;若是讀取失敗,則下一個循環再繼續嘗試。對於寫入操做,先嚐試將數據寫入指定的某個套接字,寫入失敗則下一個循環再繼續嘗試。函數

這裏寫圖片描述

這樣看來,無論有多少個套接字鏈接,它們均可以被一個線程管理,一個線程負責遍歷這些套接字列表,不斷地嘗試讀取或寫入數據。這很好地利用了阻塞的時間,處理能力獲得提高。但這種模型須要在應用程序中遍歷全部的套接字列表,同時須要處理數據的拼接,鏈接空閒時可能也會佔用較多CPU資源,不適合實際使用。對此改進的方法是使用事件驅動的非阻塞方式。學習

內核遍歷套接字的事件檢測

這種方式將套接字的遍歷工做交給了操做系統內核,把對套接字遍歷的結果組織成一系列的事件列表並返回應用層處理。對於應用層,它們須要處理的對象就是這些事件,這就是其中一種事件驅動的非阻塞方式的實現。優化

服務器端有多個客戶端鏈接,應用層向內核請求讀寫事件列表。內核遍歷全部套接字並生成對應的可讀列表readList和可寫列表writeList。readList標明瞭每一個套接字是否可讀,例如套接字1的值爲1,表示可讀,socket2的值爲0,表示不可讀。writeList則標明瞭每一個套接字是否可寫。應用層遍歷讀寫事件列表readList和writeList,作相應的讀寫操做。操作系統

這裏寫圖片描述

內核遍歷套接字時已經不用在應用層對全部套接字進行遍歷,將遍歷工做下移到內核層,這種方式有助於提升檢測效率。然而,它須要將全部鏈接的可讀事件列表和可寫事件列表傳到應用層,假如套接字鏈接數量變大,列表從內核複製到應用層也是不小的開銷。另外,當活躍鏈接較少時,內核與應用層之間存在不少無效的數據副本,由於它將活躍和不活躍的鏈接狀態都複製到應用層中。

內核基於回調的事件檢測

經過遍歷的方式檢測套接字是否可讀可寫是一種效率比較低的方式,不論是在應用層中遍歷仍是在內核中遍歷。因此須要另一種機制來優化遍歷的方式,那就是回調函數。內核中的套接字都對應一個回調函數,當客戶端往套接字發送數據時,內核從網卡接收數據後就會調用回調函數,在回調函數中維護事件列表,應用層獲取此事件列表便可獲得全部感興趣的事件。

內核基於回調的事件檢測方式有兩種。第一種是用可讀列表readList和可寫列表writeList標記讀寫事件,套接字的數量與readList和writeList兩個列表的長度同樣,readList第一個元素標爲1則表示套接字1可讀,同理,writeList第二個元素標爲1則表示套接字2可寫。如圖所示,多個客戶端鏈接服務器端,當客戶端發送數據過來時,內核從網卡複製數據成功後調用回調函數將readList第一個元素置爲1,應用層發送請求讀、寫事件列表,返回內核包含了事件標識的readList和writeList事件列表,進而分表遍歷讀事件列表readList和寫事件列表writeList,對置爲1的元素對應的套接字進行讀或寫操做。這樣就避免了遍歷套接字的操做,但仍然有大量無用的數據(狀態爲0的元素)從內核複製到應用層中。因而就有了第二種事件檢測方式。

這裏寫圖片描述

內核基於回調的事件檢測方式二如圖所示。服務器端有多個客戶端套接字鏈接。首先,應用層告訴內核每一個套接字感興趣的事件。接着,當客戶端發送數據過來時,對應會有一個回調函數,內核從網卡複製數據成功後即調回調函數將套接字1做爲可讀事件event1加入到事件列表。一樣地,內核發現網卡可寫時就將套接字2做爲可寫事件event2添加到事件列表中。最後,應用層向內核請求讀、寫事件列表,內核將包含了event1和event2的事件列表返回應用層,應用層經過遍歷事件列表得知套接字1有數據待讀取,因而進行讀操做,而套接字2則能夠寫入數據。

這裏寫圖片描述

上面兩種方式由操做系統內核維護客戶端的全部鏈接並經過回調函數不斷更新事件列表,而應用層線程只要遍歷這些事件列表便可知道可讀取或可寫入的鏈接,進而對這些鏈接進行讀寫操做,極大提升了檢測效率,天然處理能力也更強。

對於Java來講,非阻塞I/O的實現徹底是基於操做系統內核的非阻塞I/O,它將操做系統的非阻塞I/O的差別屏蔽並提供統一的API,讓咱們沒必要關心操做系統。JDK會幫咱們選擇非阻塞I/O的實現方式,例如對於Linux系統,在支持epoll的狀況下JDK會優先選擇用epoll實現Java的非阻塞I/O。這種非阻塞方式的事件檢測機制就是效率最高的「內核基於回調的事件檢測」中的第二種方式。

在瞭解了非阻塞模式下的事件檢測方式後,從新回到對單線程非阻塞I/O模型的討論。雖然只有一個線程,可是它經過把非阻塞讀寫操做與上面幾種檢測機制配合就能夠實現對多個鏈接的及時處理,而不會由於某個鏈接的阻塞操做致使其餘鏈接沒法處理。在客戶端鏈接大多數都保持活躍的狀況下,這個線程會一直循環處理這些鏈接,它很好地利用了阻塞的時間,大大提升了這個線程的執行效率。

單線程非阻塞I/O模型的主要優點體如今對多個鏈接的管理,通常在同時須要處理多個鏈接的發場景中會使用非阻塞NIO模式,此模型下只經過一個線程去維護和處理鏈接,這樣大大提升了機器的效率。通常服務器端纔會使用NIO模式,而對於客戶端,出於方便及習慣,可以使用阻塞模式的套接字進行通訊。

=============廣告時間===============

公衆號的菜單已分爲「分佈式」、「機器學習」、「深度學習」、「NLP」、「Java深度」、「Java併發核心」、「JDK源碼」、「Tomcat內核」等,可能有一款適合你的胃口。

鄙人的新書《Tomcat內核設計剖析》已經在京東銷售了,有須要的朋友能夠購買。感謝各位朋友。

爲何寫《Tomcat內核設計剖析》

=========================

歡迎關注:

這裏寫圖片描述
相關文章
相關標籤/搜索