【Netty】EventLoop和執行緒模型

leesf發表於2017-05-25

一、前言

  在學習了ChannelHandler和ChannelPipeline的有關細節後,接著學習Netty的EventLoop和執行緒模型。

二、EventLoop和執行緒模型

  2.1. 執行緒模型

  執行緒池可通過快取和複用已有執行緒來提高系統效能,基本的緩衝池模式可描述如下:

    · 從池中空閒連結串列中選取執行緒,然後將其分配賦予給已提交的任務。

    · 當執行緒完成工作時,該執行緒又返回至空閒連結串列,可再進行復用。

  該模式如下圖所示。

  

  池化和複用執行緒是針對每個任務都需要建立和銷燬執行緒的改進,但還是需要進行上下文切換,並且隨著執行緒數量的增加,其負擔也會增加。同時,在高併發下也會出現很多執行緒問題。

  2.2. EventLoop介面

  任何網路框架的基本功能都是執行任務來處理在連線宣告週期中所發生的事件,相應的程式設計結構通常被稱為事件迴圈。事件迴圈的基本思想如下程式碼所示,每個任務都是一個Runnable例項。  

while (!terminated) {
    List<Runnable> readyEvents = blockUntilEventsReady();
    for (Runnable ev: readyEvents) {
        ev.run();
    }
}

  Netty的EventLoop是使用concurrency和networking兩個基本API的協作設計的一部分,Netty中的io.netty.util.concurrent 包基於JDK的java.util.concurrent包進行設計。另外,io.netty.channel包中的類也繼承它們,以便與其事件相關聯,具體繼承關係如下圖所示。

  

  在這個模型中,EventLoop由一個永不改變的執行緒驅動,任務(Runnable或Callable)可以直接提交給EventLoop的實現,以便立即執行或有計劃地執行。根據配置和可用核心,可以建立多個EventLoops以優化資源使用,並且可以為單個EventLoop分配服務多個通道。

  事件和任務以FIFO的方式被執行,這通過保證以正確的順序處理位元組內容來消除資料損壞的可能性。

  1. Netty 4中的I/O和事件處理

  由I/O操作觸發的事件流過具有一個或多個ChannelHandler的ChannelPipeline時,傳播這些事件的方法呼叫可以由ChannelHandler攔截,並根據需要進行處理,根據事件的不同,需要進行不同的處理,但事件處理邏輯必須具有通用性和靈活性,以處理所有可能的用例,因此,在Netty 4中,所有的I/O操作和事件都由已分配給EventLoop的執行緒處理。

  2. Netty 3中的I/O處理

  以前版本中使用的執行緒模型僅保證入站(上游)事件將在所謂的I/O執行緒中執行,所有出站(下游)事件由呼叫執行緒處理,其需要在ChannelHandlers中仔細同步出站事件,因為不可能保證多個執行緒不會同時嘗試訪問出站事件。

  2.3 任務排程

  有時需要讓一個任務稍後(延遲)或定期執行,一個常見的用例是向遠端對等體傳送心跳訊息,以檢查連線是否仍然存在。

  1. JDK排程API

  在Java 5之前,任務排程基於java.util.Timer構建,其使用後臺執行緒,與標準執行緒具有相同的限制,隨後,Java提供了ScheduledExecutorService介面,如下程式碼在60S後執行任務。  

ScheduledExecutorService executor = Executors.newScheduledThreadPool(10);
ScheduledFuture<?> future = executor.schedule(
    new Runnable() {
        @Override
        public void run() {
            System.out.println("60 seconds later");
        }
    }, 60, TimeUnit.SECONDS);
    
executor.shutdown();

  2. 使用EventLoop排程任務

  ScheduledExecutorService實現有限制,如為管理池需要建立額外的執行緒,如果許多工被排程,這可能會成為系統效能瓶頸。Netty通過使用Channel的EventLoop排程來解決這個問題,如下程式碼所示。  

Channel ch = ...
ScheduledFuture<?> future = ch.eventLoop().schedule(
    new Runnable() {
    @Override
    public void run() {
        System.out.println("60 seconds later");
    }
}, 60, TimeUnit.SECONDS);

  60秒後,Runnable例項將由分配給該Channel的EventLoop執行。若想每隔60S執行任務,則需要做如下處理。  

Channel ch = ...
ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(
    new Runnable() {
    @Override
    public void run() {
        System.out.println("Run every 60 seconds");
    }
}, 60, 60, TimeUnit.Seconds);

  因為EventLoop繼承ScheduledExecutorService,因此可以呼叫ScheduledExecutorService的所有方法。

  2.4 實現細節

  1. 執行緒管理

  Netty的執行緒模型的優越效能取決於確定當前正在執行的執行緒的身份,即是否為分配給當前Channel及其EventLoop的執行緒。如果呼叫的是EventLoop的執行緒,那麼直接執行該程式碼塊,否則,EventLoop排程一個任務以供稍後執行,並將其放入內部佇列中,當處理下個事件時,會處理佇列中的事件,這解釋了任何執行緒為何可以直接與Channel互動,而不需要在ChannelHandler中同步。

  每個EventLoop都有自己的任務佇列,與其他EventLoop獨立,下圖顯示了EventLoop的執行邏輯。

  

  不要把長時間的任務放在執行佇列中,因為它將阻止任何其他任務在同一個執行緒上執行。如果必須進行阻塞呼叫或執行長時間執行的任務,建議使用專用的EventExecutor。

  2. EventLoop/執行緒的分配

  為通道的I/O和事件提供服務的EventLoops包含在EventLoopGroup,EventLoops建立和分配的方式根據傳輸實現(非同步和阻塞)而有所不同。

  · 非同步傳輸。只使用少量的EventLoopGroup,在當前的模型中其在通道中共享。這允許通道由最小數量的執行緒提供服務,而不是為每個通道分配一個執行緒。下圖展示了包含三個EventLoop(每個EventLoop由一個執行緒驅動)的EventLoopGroup,EventLoopGroup建立時會直接分配EventLoops(及其執行緒),以確保它們在需要時可用,EventLoopGroup負責將EventLoop分配給每個新建立的通道,當前的實現是使用迴圈方法實現均衡分配,相同的EventLoop可被分配給多個通道

  

  一旦一個Channel被分配了一個EventLoop,它將在其生命週期中一直使用這個EventLoop(和相關聯的執行緒)。同時請注意EventLoop的分配對ThreadLocal影響,因為一個EventLoop通常驅動多個通道,多個通道的ThreadLocal也相同。

  · 阻塞傳輸。OIO的實現與非同步傳輸的實現大不相同,其如下圖所示。

  

  每個通道將會分配一個EventLoop(以及相關執行緒),Channel的IO事件將由獨立的執行緒處理。

三、總結

  本篇博文講解了EventLoop及其執行緒模型,以及其與通道之間的關係,EventLoopGroup可對應多個EventLoop,一個EventLoop對應一個執行緒,一個EventLoop可對應多個通道。也謝謝各位園友的觀看~

相關文章