1. 程式人生 > >深入研究Netty之執行緒模型詳解

深入研究Netty之執行緒模型詳解

https://my.oschina.net/7001/blog/1480153

本文主要介紹Netty執行緒模型及其實現,介紹Netty執行緒模型前,首先會介紹下經典的Reactor執行緒模型,目前大多數網路框架都是基於Reactor模式進行設計和開發,Reactor模式基於事件驅動,非常適合處理海量的I/O事件。下面簡單介紹下Reactor模式及其執行緒模型。

Reactor模式

Reactor模式首先是事件驅動的,有一個或多個併發輸入源,有一個Service Handler,有多個Request Handlers;這個Service Handler會同步的將輸入的請求(Event)多路複用的分發給相應的Request Handler。下面先回顧下Reactor執行緒模型。

單執行緒模型

單執行緒模型下,所有的IO操作都由同一個Reactor執行緒來完成,其主要職責如下:

  • 作為服務端,接收客戶端的TCP連線;
  • 作為客戶端,向服務端發起TCP連線;
  • 讀取通訊對端的請求或者應答訊息;
  • 向通訊對端傳送訊息請求或者應答訊息。

Reactor單執行緒模型原理圖如下:

如圖所示,由於Reactor模式使用的是非同步非阻塞IO,所有的IO操作都不會導致阻塞。通常Reactor執行緒中聚合了多路複用器負責監聽網路事件,當有新連線到來時,觸發連線事件,Disdatcher負責使用Acceptor接受客戶端連線,建立通訊鏈路;當I/O事件就緒後,Disdatcher負責將事件分發到對應的event handler上負責處理。

該模型的缺點很明顯,不適用於高負載、高併發的應用場景;由於只有一個Reactor執行緒,一旦掛彩,整個系統通訊模組將不可用。

多執行緒模型

先看原理圖:

該模型的特點:

  • 專門由一個Reactor執行緒-Acceptor執行緒用於監聽服務端,接收客戶端連線請求;
  • 網路I/O操作讀、寫等由Reactor執行緒池負責處理;
  • 一個Reactor執行緒可同時處理多條鏈路,但一條鏈路只能對應一個Reactor執行緒,這樣可避免併發操作問題。

絕大多數場景下,Reactor多執行緒模型都可以滿足效能需求,但是,在極個別特殊場景中,一個Reactor執行緒負責監聽和處理所有的客戶端連線可能會存在效能問題。例如併發百萬客戶端連線,或者服務端需要對客戶端握手進行安全認證,但是認證本身非常損耗效能。因此,誕生了第三種執行緒模型。

主從多執行緒模型

先看原理圖:

該模型的特點:

  • 服務端使用一個獨立的主Reactor執行緒池來處理客戶端連線,當服務端收到連線請求時,從主執行緒池中隨機選擇一個Reactor執行緒作為Acceptor執行緒處理連線;
  • 鏈路建立成功後,將新建立的SocketChannel註冊到sub reactor執行緒池的某個Reactor執行緒上,由它處理後續的I/O操作。

Netty執行緒模型

Netty同時支援Reactor單執行緒模型 、Reactor多執行緒模型和Reactor主從多執行緒模型,使用者可根據啟動引數配置在這三種模型之間切換。Netty執行緒模型原理圖如下:

服務端啟動時,通常會建立兩個NioEventLoopGroup例項,對應了兩個獨立的Reactor執行緒池。常見服務端啟動程式碼實現如下:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
    ServerBootstrap b = new ServerBootstrap();
    b.group(bossGroup, workerGroup)
     .channel(NioServerSocketChannel.class)
     .option(ChannelOption.SO_BACKLOG, 100)
     .handler(new LoggingHandler(LogLevel.INFO))
     .childHandler(new ChannelInitializer<SocketChannel>() {
         @Override
         public void initChannel(SocketChannel ch) throws Exception {
               ......

bossGroup負責處理客戶端的連線請求,workerGroup負責處理I/O相關的操作,執行系統Task、定時任務Task等。

使用者可根據服務端引導類ServerBootstrap配置引數選擇Reactor執行緒模型,進而最大限度地滿足使用者的定製化需求;同時,為了最大限度地提升效能,netty很多地方採用了無鎖化設計,如為每個Channel繫結唯一的EventLoop,這意味著同一個Channel生命週期內的所有事件都將由同一個Reactor執行緒來完成,這種序列化處理方式有效地避免了多執行緒操作之間鎖的競爭和上下文切換帶來的開銷。此外,每個Reactor執行緒配備了一個task佇列和Delay task佇列,分別用於存放系統Task和週期性Task,也就是說每個Reactor執行緒不僅要處理I/O事件,還會處理一些系統任務和排程任務。

EventLoop家族

EventLoop又叫事件迴圈,旨在通過執行任務來處理在連線的生命週期內發生的事件。Netty的EventLoop是協同設計的一部分,主要採用了兩個基本的API:併發和網路程式設計。首先,io.netty.util.concurrent包構建在JDK的java.util.concurrent包上,用來提供執行緒執行器。其次,io.netty.channel包中的類,為了與Channel的事件進行互動,擴充套件了io.netty.util.concurrent包中的介面和類。下面通過類圖來說明:

類圖

成員簡介

首先從io.netty.util.concurrent開始:

EventExecutorGroup:字面含義是事件執行器組,管理著一組EventExecutor,負責通過其next()方法提供EventExecutor的使用,並負責管理這些EventExecutor的生命週期。EventExecutorGroup擴充套件了JDK的ScheduledExecutorService,使得其子類具有提交執行排程任務的能力;同時還擴充套件了Iterable,說明EventExecutorGroup是可迭代的。此外,EventExecutorGroup還定義了優雅退出的方法。

AbstractEventExecutorGroup:EventExecutorGroup的抽象實現,並沒有提供新的API,只是簡單的為EventExecutorGroup中定義的方法提供了預設實現。EventExecutorGroup本身並不能執行任務,它首先通過next()選擇一個EventExecutor物件,然後將執行任務的工作都是委託給這個物件。換句話說,具體實現由EventExecutor的子類來完成。

MultithreadEventExecutorGroup:EventExecutorGroup的抽象實現,內部組合了多個EventExecutor用於對外提供服務,並負責管理一組EventExecutor例項。它還提供了抽象方法newChild用於構造EventExecutor例項,由子類提供實現,便於構造定製化的EventExecutor。它還聚合了一個EventExecutorChooser物件,用於定製通過next從陣列中選擇EventExecutor物件的規則。

DefaultEventExecutorGroup:EventExecutorGroup的預設實現,當需要使用DefaultEventExecutor來執行任務時,可使用該實現,比較少用。

EventExecutor:事件執行器,它是一個只使用一個執行緒來執行任務的特殊執行緒池,其擴充套件了EventExecutorGroup,主要是為了方便代理EventExecutorGroup中的方法;此外,EventExecutor中也定義一些自己的API,如:用於識別執行緒身份的方法inEventLoop,建立各種通知器Promise的方法。

AbstractEventExecutor:EventExecutor的抽象實現,其實現了EventExecutorGroup中的抽象方法,提交任務的方法委託給父類AbstractExecutorService來完成,但不支援提交排程任務,呼叫schedule相關方法都會丟擲UnsupportedOperationException。此外,為了實現iterator,其內部定義了一個只能包含一個元素的Collection,且這個元素就是當前EventExecutor例項,因此迭代AbstractEventExecutor只會返回自身例項。使用者可直接呼叫next方法,預設也是返回當前EventExecutor例項,更方便快捷。

AbstractScheduledEventExecutor:EventExecutor的抽象實現,主要為了支援排程任務的執行。其持有一個PriorityQueue用於存放排程任務,並實現了ScheduledExecutorService中定義的提交排程任務的方法。

OrderedEventExecutor:它是一個標識介面,沒有任何方法和屬性,僅僅表明實現該介面的類擁有按順序序列執行任務的能力。

SingleThreadEventExecutor:一個可執行普通任務和排程任務的單執行緒執行器,也就是說,提交到該執行緒池的所有任務都有同一個執行緒來完成。內部具有一個阻塞佇列用於儲存所有提交到該執行器的任務。同時,其擴充套件了OrderedEventExecutor,表明需要按順序序列執行所有提交的任務,這裡體現了Netty無鎖化設計。該介面實現了執行任務內部運作邏輯,後續會對其原始碼進行深入分析。即使如此,由於它還沒有和特定的Selector繫結,因此不能執行I/O相關的操作。

io.netty.channel:

EventLoopGroup:Event Loop執行緒組,用於管理一組EventLoop對外提供服務,其擴充套件了EventExecutorGroup,同時定義了註冊Channel的方法,用於將一個EventLoop與Channel繫結。

MultithreadEventLoopGroup:EventLoopGroup的抽象實現,初始化時會確認用於IO操作的EventLoop執行緒數量,預設值是處理器個數的2倍。從EventLoopGroup繼承的register方法主要委託給next返回的EventLoop來完成。同時還擴充套件了MultithreadEventExecutorGroup,這樣從EventExecutorGroup繼承的方法都得到預設實現,但從MultithreadEventExecutorGroup中繼承的newChild沒有預設實現,它需要由由最終子類來實現。

DefaultEventLoopGroup:用於只能用於本地傳輸的EventLoop實現。

EventLoop:一旦與Channel繫結,將處理該Channel上的所有I/O操作。EventLoop可同時處理多個Channel中I/O操作,也可以只處理一個Channel上的I/O操作,具體由不同網路I/O確定。如Oio只能處理單個Channel的I/O操作,NIO則可以處理多個Channel的I/O操作。EventLoop所有子類都將順序序列執行任務,因為其擴充套件了OrderedEventExecutor。

SingleThreadEventLoop:EventLoop的抽象實現,同時擴充套件了SingleThreadEventExecutor,負責用單個執行緒來執行所有提交到當前EventLoop的任務。SingleThreadEventLoop內部持有一個tailTasks佇列,不知道幹嘛用,目前內部也沒有任何地方呼叫。SingleThreadEventLoop中主要實現了register相關方法。不同網路I/O型別通過擴充套件該類來完成底層實現。

ThreadPerChannelEventLoop:主要用於Oio的EventLoop實現,一個EventLoop只處理一個Channel的I/O操作。

io.netty.channel.nio:

NioEventLoopGroup:擴充套件自MultithreadEventLoopGroup,定義NIO的獨特實現,主要實現了newChild方法。對於使用者,程式碼中使用較多的也就NioEventLoopGroup了。

NioEventLoop:NIO實現,內部聚合了Java Selector,使得EventLoop成為一個真正意義的Reactor執行緒。內部除了實現Selector相關的一些操作,同時實現了執行任務的核心邏輯run方法。後續會詳細分析它的原始碼。

執行緒管理

Netty執行緒模型的卓越效能取決於它對當前執行的Thread的身份確定,也就是說,確定他是否是分配給當前Channel以及它的EventLoop的那個執行緒(通過呼叫inEventLoop(Thread))。

如果當前呼叫執行緒正是支撐EventLoop的執行緒,那麼所提交的程式碼塊都將被直接執行。否則,EventLoop將排程該任務以便以後執行,並將它放入到內部佇列中。當EventLoop下次處理它的事件時,它會執行佇列中的任務、事件。這也解釋了任何的Thread是如何與Channel直接互動而無需在ChannelHandler中進行額外同步的。不過,這僅對Netty4或更高的版本有效,在Netty3中只保證了入站事件在EventLoop對應的執行緒中執行,所有的出站事件都由呼叫執行緒處理,呼叫執行緒可能是EventLoop執行緒也可能是別的執行緒,因此,需要在ChannelHandler中對出站事件的處理進行同步,保證執行緒安全。

每個EventLoop都有它自己的任務佇列,獨立於其他的EventLoop。實際開發過程中,絕不應該阻塞當前I/O執行緒,或是將一個長時間執行的任務放入任務佇列,因為它將阻塞需要在同一個執行緒上執行的任何其他任務。

此外,需注意EventLoop的分配方式對ThreadLocal使用的影響。由於NIO實現中EventLoop通常用於支援多個Channel,所以對於所有相關聯的Channel來說,ThreadLocal都是一樣的。

執行緒模型選擇

下面以服務端的配置為例,說明如何選擇不同的執行緒模型。

單執行緒模型

 EventLoopGroup bossGroup = new NioEventLoopGroup(1);
 try {
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup)
         .channel(NioServerSocketChannel.class)
        ......

以上示例中例項化了一個NIOEventLoopGroup,並傳入執行緒數量為1,然後呼叫ServerBootstrap的group方法繫結執行緒組,看實現:

    @Override
    public ServerBootstrap group(EventLoopGroup group) {
        return group(group, group);
    }
    public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
        super.group(parentGroup);
        if (childGroup == null) {
            throw new NullPointerException("childGroup");
        }
        if (this.childGroup != null) {
            throw new IllegalStateException("childGroup set already");
        }
        this.childGroup = childGroup;
        return this;
    }

從原始碼可知,實際仍然綁定了 bossGroup 和 workerGroup,只是都是同一個NioEventLoopGroup例項而已,這樣Netty中的acceptor和後續的所有客戶端連線的IO操作都是在一個執行緒中處理,這就相當於Reactor的單執行緒模型。

多執行緒模型

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
      ServerBootstrap b = new ServerBootstrap();
      b.group(bossGroup, workerGroup)
       .channel(NioServerSocketChannel.class)
       ......

建立1個執行緒的bossGroup執行緒組,這個執行緒負責處理客戶端的連線請求,而workerGroup預設使用處理器個數*2的執行緒數量來處理I/O操作。這就相當於Reactor的多執行緒模型。

主從多執行緒模型

EventLoopGroup bossGroup = new NioEventLoopGroup(4);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
      ServerBootstrap b = new ServerBootstrap();
      b.group(bossGroup, workerGroup)
       .channel(NioServerSocketChannel.class)
       ......