Netty學習三:線程模型


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線程模型中的職責如下:

1. 作為服務端Acceptor線程,負責處理客戶端的請求接入
2. 作為客戶端Connecor線程,負責注冊監聽連接操作位,用於判斷異步連接結果
3. 作為IO線程,監聽網絡讀操作位,負責從SocketChannel中讀取報文
4. 作為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

epoll-bug的處理

在netty中對java nio的epoll bug進行了處理,就是設置一個閥值,如果超過了就rebuidSelector來避免epoll()死循環

3.2.4 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中,這樣可以確保這些操作都是線程安全的


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM