Netty框架學習之(四):執行緒模型
轉載自:https://www.cnblogs.com/TomSnail/p/6158249.html
1. Proactor和Reactor
Proactor和Reactor是兩種經典的多路複用I/O模型,主要用於在高併發、高吞吐量的環境中進行I/O處理。
I/O多路複用機制都依賴於一個事件分發器,事件分離器把接收到的客戶事件分發到不同的事件處理器中,如下圖:
1.1 select,poll,epoll
在作業系統級別select,poll,epoll是3個常用的I/O多路複用機制,簡單瞭解一下將有助於我們理解Proactor和Reactor。
1.1.1 select
select的原理如下:
使用者程式發起讀操作後,將阻塞查詢讀資料是否可用,直到核心準備好資料後,使用者程式才會真正的讀取資料。
poll與select的原理相似,使用者程式都要阻塞查詢事件是否就緒,但poll沒有最大檔案描述符的限制。
1.1.2 epoll
epoll是select和poll的改進,原理圖如下:
epoll使用“事件”的方式通知使用者程式資料就緒,並且使用記憶體拷貝的方式使使用者程式直接讀取核心準備好的資料,不用再讀取資料
1.2 Proactor
Proactor是一個非同步I/O的多路複用模型,原理圖如下:
- 使用者發起IO操作到事件分離器
- 事件分離器通知作業系統進行IO操作
- 作業系統將資料存放到資料快取區
- 作業系統通知分發器IO完成
- 分離器將事件分發至相應的事件處理器
- 事件處理器直接讀取資料快取區內的資料進行處理
1.3 Reactor
Reactor是一個同步的I/O多路複用模型,它沒有Proactor模式那麼複雜,原理圖如下:
- 使用者發起IO操作到事件分離器
- 事件分離器呼叫相應的處理器處理事件
- 事件處理完成,事件分離器獲得控制權,繼續相應處理
1.4 Proactor和Reactor的比較
- Reactor模型簡單,Proactor複雜
- Reactor是同步處理方式,Proactor是非同步處理方式
- Proactor的IO事件依賴作業系統,作業系統須支援非同步IO
- 同步與非同步是相對於服務端與IO事件來說的,Proactor通過作業系統非同步來完成IO操作,當IO完成後通知事件分離器,而Reactor需要自己完成IO操作
2 Reactor多執行緒模型
前面已經簡單介紹了Proactor和Reactor模型,在實際中Proactor由於需要作業系統的支援,實現的案例不多,有興趣的可以看一下Boost Asio的實現,我們主要說一下Reactor模型,Netty也是使用Reactor實現的。
但單執行緒的Reactor模型每一個使用者事件都在一個執行緒中執行:
效能有極限,不能處理成百上千的事件
當負荷達到一定程度時,效能將會下降
單某一個事件處理器傳送故障,不能繼續處理其他事件
2.1 多執行緒Reactor
使用執行緒池的技術來處理I/O操作,原理圖如下:
- Acceptor專門用來監聽接收客戶端的請求
- I/O讀寫操作由執行緒池進行負責
- 每個執行緒可以同時處理幾個鏈路請求,但一個鏈路請求只能在一個執行緒中進行處理
2.2 主從多執行緒Reactor
在多執行緒Reactor中只有一個Acceptor,如果出現登入、認證等耗效能的操作,這時就會有單點效能問題,因此產生了主從Reactor多執行緒模型,原理如下:
- Acceptor不再是一個單獨的NIO執行緒,而是一個獨立的NIO執行緒池
- Acceptor處理完後,將事件註冊到IO執行緒池的某個執行緒上
- IO執行緒繼續完成後續的IO操作
- Acceptor僅僅完成登入、握手和安全認證等操作,IO操作和業務處理依然在後面的從執行緒中完成
3 Netty中Reactor模型的實現
Netty同時支援Reactor的單執行緒、多執行緒和主從多執行緒模型,在不同的應用中通過啟動引數的配置來啟動不同的執行緒模型。
通過執行緒池的執行緒個數、是否共享執行緒池方式來切換不同的模型
3.1 Netty中的Reactor模型
Netty中的Reactor模型如下圖:
- Acceptor中的NioEventLoop用於接收TCP連線,初始化引數
- I/O執行緒池中的NioEventLoop非同步讀取通訊對端的資料報,傳送讀事件到channel
- 非同步傳送訊息到對端,呼叫channel的訊息傳送介面
- 執行系統呼叫Task
- 執行定時Task
3.2 NioEventLoop
NioEventLoop是Netty的Reactor執行緒,它在Netty Reactor執行緒模型中的職責如下:
- 作為服務端Acceptor執行緒,負責處理客戶端的請求接入
- 作為客戶端Connecor執行緒,負責註冊監聽連線操作位,用於判斷非同步連線結果
- 作為IO執行緒,監聽網路讀操作位,負責從SocketChannel中讀取報文
- 作為IO執行緒,負責向SocketChannel寫入報文傳送給對方,如果發生寫半包,會自動註冊監聽寫事件,用於後續繼續傳送半包資料,直到資料全部發送完成
如下圖,是一個NioEventLoop的處理鏈:
- 處理鏈中的處理方法是序列化執行的
- 一個客戶端連線只註冊到一個NioEventLoop上,避免了多個IO執行緒併發操作
3.2.1 Task
Netty Reactor執行緒模型中有兩種Task:系統Task和定時Task
- 系統Task:建立它們的主要原因是,當IO執行緒和使用者執行緒都在操作同一個資源時,為了防止併發操作時鎖的競爭問題,將使用者執行緒封裝為一個Task,在IO執行緒負責執行,實現區域性無鎖化
- 定時Task:主要用於監控和檢查等定時動作
基於以上原因,NioEventLoop不是一個純粹的IO執行緒,它還會負責使用者執行緒的排程
3.2.2 IO執行緒的分配細節
執行緒池對IO執行緒進行資源管理,是通過EventLoopGroup實現的。執行緒池平均分配channel到所有的執行緒(迴圈方式實現,不是100%準確),一個執行緒在同一時間只會處理一個通道的IO操作,這種方式可以確保我們不需要關心同步問題。
3.2.3 Selector
NioEventLoop是Reactor的核心執行緒,那麼它就就必須實現多路複用。
Selector的過程如下:
- 首先oldWakenUp = wakenUp.getAndSet(false)
- 如果佇列中有任務, selectNow()
- 如果沒有select(),直達channel準備就緒,但此過程中迴圈次數超過限值也將rebuidSelectoror退出迴圈
- 執行processSelectedKeys和runAllTasks
3.2.4 epoll-bug的處理
在netty中對java nio的epoll bug進行了處理,就是設定一個閥值,如果超過了就rebuidSelector來避免epoll()死迴圈
3.2.5 NioEevntLoopGroup
EventExecutorGroup:提供管理EevntLoop的能力,他通過next()來為任務分配執行執行緒,同時也提供了shutdownGracefully這一優雅下線的介面.
EventLoopGroup繼承了EventExecutorGroup介面,並新添了3個方法
- EventLoop next()
- ChannelFuture register(Channel channel)
- ChannelFuture register(Channel channel, ChannelPromise promise)
EventLoopGroup的實現中使用next().register(channel)來完成channel的註冊,即將channel註冊時就綁定了一個EventLoop,然後EvetLoop將channel註冊到EventLoop的Selector上。
NioEventLoopGroup還有幾點需要注意:
- NioEventLoopGroup下預設的NioEventLoop個數為cpu核數 * 2,因為有很多的io處理
- NioEventLoop和java的single執行緒池在5裡差異變大了,它本身不負責執行緒的建立銷燬,而是由外部傳入的執行緒池管理
- channel和EventLoop是繫結的,即一旦連線被分配到EventLoop,其相關的I/O、編解碼、超時處理都在同一個EventLoop中,這樣可以確保這些操作都是執行緒安全的