【Netty】EventLoop和線程模型


一、前言

  在學習了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可對應多個通道。也謝謝各位園友的觀看~


免責聲明!

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



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