Netty4.x源碼分析之EventLoop(一)

引言

上篇文章我們分析了ServerBootstrap的啓動這部分源碼,從整體的角度初步接觸了netty的編碼設計的魅力,今天我們來分析一下Netty裏面另一個核心角色–EventLoop。用過netty的同學應該都或多或少知道Netty的高性能很大一部分功勞都在EventLoop的設計,尤其是EventLoop對Reactor線程模型的實現,大大提升了Netty的併發能力,接下來我們就來揭開EventLoop的神祕面紗

UNIX網絡I/O模型

Netty一直以其優異的性能表現收穫粉絲無數,有研究稱netty可以撐過單機10w的TPS。如此高的性能自然離不開精良的線程模型設計和UNIX網絡I/O編程模型的選用,我們在看源碼前先了解下UNIX網了I/O編程模型和Reactor線程模型。

UNIX網絡I/O模型本來有五種,這裏只列出三種與我們理解Netty源碼有關的,即阻塞I/O模型、非阻塞I/O模型、I/O多路複用模型。

阻塞I/O模型 在JDK1.4之前默認使用的就是阻塞I/O模型,阻塞I/O模型所有的文件操作都是阻塞的。我們以套接字接口爲例:在進程空間中調用recvfrom,系統調用知道數據包到達並且被複制到應用進程(用戶態)的緩衝區或者發生錯誤時才返回,在返回之前一直等待,進程從調用recvfrom到返回一直被阻塞。瞭解這一點,我們就知道爲什麼JDK1.4之前java的網絡通信爲什麼如此爲人所詬病了,只要併發量上來,假設因爲某些客觀原因網絡擁堵,很有可能會有大量accept進來的客戶端連接會被阻塞,導致大量連接超時。

非阻塞I/O模型 非阻塞I/O模型與阻塞I/O模型的唯一區別就是recvfrom系統調用發出後,如果應用進程的緩存區沒有數據,就直接返回一個EWOULDBLOCK的錯誤,一般都是使用一個線程進行輪詢這個狀態,看內核是否有數據到來。這個非阻塞I/O模型對於阻塞I/O模型是個進步,但是爲每個連接都是用一個線程進行輪詢,這會對cpu時間大量的耗費,性能也不高。

I/O多路複用模型 進程通過將一個或多個文件描述符(fd)傳遞給select/poll系統調用,阻塞在select操作上,雖然都會阻塞,但是可以實現多個fd阻塞在這個select上,這樣我們通過一個線程就可以使用select/poll監聽多個文件描述符是否處於就緒狀態,這個進步是巨大的,一個線程可以監聽大量的連接情況,Java的NIO就是使用的I/O多路複用模型,而Netty就是使用了Java的NIO。

Reactor線程模型

Reactor是一個形象的名稱,具體下來可以理解爲這麼一段僞代碼 while(true) {selector.select();後續處理…} ,就是不斷的select一把,然後對select出來的連接進行對應的業務操作。Reactor模型實質上是一個觀察者模式,所有的事件處理器註冊在Dispatcher上,只要select出多個連接,並監聽感興趣的事件,Dispatcher就調用這些對應的事件處理器,並且這些操作是異步的。Reactor線程模型有三種:Reactor單線程模型、Reactor多線程模型、主從多線程模型。

Reactor單線程模型
Reactor單線程模型比較簡單,就是一個線程把select操作、監聽讀寫事件,處理業務操作全在一個線程內,雖然簡單但也強大,因爲使用的I/O多路複用模型,除非你的業務邏輯比較耗時,一般體量的業務Reactor單線程模型就足夠了。但是一旦併發連接數上來恐怕是hold不住,單線程無法滿足大量的消息編解碼、消息讀取和發送,可能會導致大量的連接超時。基於這些問題演進了Reactor多線程模型。

Reactor多線程模型
Reactor多線程模型爲了解決單線程無法滿足大量消息編解碼,I/O事件的處理,將Accept事件和讀寫I/O事件區分開,Accept事件單獨使用單線程處理,因Accept操作簡單,資源佔用少,所以單線程足以支撐海量連接的Accept的操作。而I/O讀寫事件通過一個線程池處理.Reactor多線程模型充分利用了現代計算機多核cpu的特性,大大提升了TCPServer的併發性能。而Netty正是使用了Reactor多線程模型,只不過在Netty中對多線程模型的線程池做了適應性改造,一個連接綁定一個線程,而不是像JDK原生線程池一樣通過一個隊列接收所有的task,多個線程競爭獲取task執行。這樣減少了因爲同步導致的資源消耗,一定程度上提高了性能。除了Reactor多線程模型,還有一種Reactor主從多線程模型。

Reactor主從多線程模型
其實一般項目使用Reactor多線程模型足夠的,目前本人並不太理解這個主從多線程模型的需求,因爲Accept操作並不太消耗資源,加上使用的是I/O多路複用模型,一個線程處理足以大量的Accept請求。儘管如此我們還是簡單瞭解一下主從多線程模型吧,有對主從多線程模型理解更深的歡迎交流。

主從多線程模型除了對Accept操作把單線程換成一個線程池之外,其他的和Reactor多線程模型一模一樣。應該是覺得可能在Accept階段如果使用單線程處理會成爲一個瓶頸,所以Accept階段也使用一個線程池處理,查看李林峯老師的Netty權威指南也指明瞭很多項目有安全需求,比如服務端需要對客戶端握手進行安全認證,安全認證的過程是比較消耗性能的,這種場景下單線程處理Accept過程可能存在性能問題。

NioEventLoopGroup初始化

瞭解了UNIX網絡I/O模型和Reactor線程模型之後我們正式開始瞭解Netty的EventLoop。我們可以回憶一下我們是怎麼在啓動ServerBootstrap時設置EventLoop的.

this.bossGroup = new NioEventLoopGroup(bossThreadNumber, new DefaultThreadFactory(bossThreadName, true));
        this.workerGroup = new NioEventLoopGroup(workerThreadNumber, new DefaultThreadFactory(workerThreadName, true));

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(this.bossGroup, this.workerGroup).channel(NioServerSocketChannel.class).childHandler(channelInitializer);

我們創建兩個NioEventLoopGroup實例,從名稱可以大概猜到這兩個是線程池,一個處理三次握手的Accept過程,一般我們稱爲Boss線程池,一個是處理讀寫I/O的網絡事件,一般我們稱爲worker線程池。我們先看看這個NioEventLoopGroup的類層次結構
在這裏插入圖片描述
可以看到NioEventLoopGroup類的繼承體系還是比較複雜的,首先繼承了MultithreadEventLoopGroup,然後繼承MultithreadEventExecutorGroup,AbstractEventExecutorGroup,並且還實現了ScheduledExecutorService接口,我們可以初步判斷這個NioEventLoopGroup不僅僅是個線程池可以提交任務執行,還能提交定時任務執行。接下來我們看看NioEventLoopGroup的初始化。
在這裏插入圖片描述
可以看到NioEventLoop經過自己的構造函數多層調用後拿到了線程數、threadFactory、selectorProvider,selectStrategyFactory和rejectHandler調用了父類MultithreadEventLoopGroup的構造函數。其中線程數和threadFactory我們不用多說大家都清楚,而selectorProvider就是提供selector對象的,而selectStrategyFactory這裏用不到先不說。
接下來MultithreadEventLoopGroup又經過層層調用拿到了ThreadPreTaskExecutor實例和DefaultEventExecutorChooserFactory實例,這裏ThreadPreTaskExecutor實現了Executor接口,所以ThreadPreTaskExecutor基本實現了線程池的功能–創建線程和啓動線程。而這個DefaultEventExecutorChooserFactory則是多線程中選擇的負載均衡器,說白了就是線程選擇器。最終調用到了MultithreadEventLoopGroup關鍵的構造函數,我們看看代碼

    protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
      //去除無關代碼
        children = new EventExecutor[nThreads];

        for (int i = 0; i < nThreads; i ++) {
            boolean success = false;
            try {
                children[i] = newChild(executor, args);
                success = true;
            } catch (Exception e) {
                // TODO: Think about if this is a good exception type
                throw new IllegalStateException("failed to create a child event loop", e);
            } 
    
        chooser = chooserFactory.newChooser(children);
    }

在這個關鍵的構造函數裏面初始化了nthreads個EventExecutor數組,for循環創建子線程,這個newChild方法是個抽象方法,實現在NioEventLoopGroup中。

@Override
    protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        return new NioEventLoop(this, executor, (SelectorProvider) args[0],
            ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
    }

進入NioEventLoop類中我們可以看到該類也很複雜,查看類層次結構,可以看到它的父類維護了thread的屬性,所以我們可以認爲NioEventLoop其實是線程包裝類,所以我們可以認爲這個EventExecutor數組其實是NioEventLoop–線程包裝類數組。所以我們已經可以斷定這個MultithreadEventLoopGroup其實就是線程池。

NioEventLoop

NioEventLoop類層次結構也很複雜,首先它繼承SingleThreadEventLoop,看到類名大概也知道了這個NioEventLoop首先是個單線程包裝類,然後SingleThreadEventLoop繼承SingleThreadEventExecuto、AbstractScheduledEventExecutor,最上層還實現了ScheduledExecutorService,所以NioEventLoop還實現了scheduled的邏輯可以執行定時調度任務。我們看看類圖
在這裏插入圖片描述
首先我們看看NioEventLoop的構造函數

NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
                 SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
        super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
        if (selectorProvider == null) {
            throw new NullPointerException("selectorProvider");
        }
        if (strategy == null) {
            throw new NullPointerException("selectStrategy");
        }
        provider = selectorProvider;
        final SelectorTuple selectorTuple = openSelector();
        selector = selectorTuple.selector;
        unwrappedSelector = selectorTuple.unwrappedSelector;
        selectStrategy = strategy;
    }

NioEventLoop的構造函數拿到了selectorProvider和selectStrategy,並賦值給全局變量,最後通過openSelector()方法拿到一個selector對象,並通過selectorTuple包裝好。到這我們大概可以知道每個線程持有一個selector對象,聯繫到上篇文章分析的channel的註冊過程,我們應該知道每個線程應該會通過一個忙循環來select一把來監聽所有感興趣的I/O事件,最終通過pipeline把事件傳到業務handler中對這些I/O事件進行處理。最後我們看看剛剛說的父類SingleThreadEventExecutor類構造函數。

private final Queue<Runnable> taskQueue;

    private volatile Thread thread;
    @SuppressWarnings("unused")
    private volatile ThreadProperties threadProperties;
    private final Executor executor;
    private volatile boolean interrupted;

    private final Semaphore threadLock = new Semaphore(0);
    private final Set<Runnable> shutdownHooks = new LinkedHashSet<Runnable>();
    private final boolean addTaskWakesUp;
    private final int maxPendingTasks;
    private final RejectedExecutionHandler rejectedExecutionHandler;

    private long lastExecutionTime;

    @SuppressWarnings({ "FieldMayBeFinal", "unused" })
    private volatile int state = ST_NOT_STARTED;
    
protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
                                        boolean addTaskWakesUp, int maxPendingTasks,
                                        RejectedExecutionHandler rejectedHandler) {
        super(parent);
        this.addTaskWakesUp = addTaskWakesUp;
        this.maxPendingTasks = Math.max(16, maxPendingTasks);
        this.executor = ObjectUtil.checkNotNull(executor, "executor");
        taskQueue = newTaskQueue(this.maxPendingTasks);
        rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
    }

可以看到它持有了一個thread,並且還在構造函數中new了一個TaskQueue可以接收任務執行,這個TaskQueue也證實了Netty的EventLoop不僅可以執行I/O任務,也可以把業務任務提交到TaskQueue裏執行用戶非I/O任務的業務任務。

NioEventLoop啓動

經過NioEventLoopGroup的初始化分析,我們知道了NioEventLoopGroup是個線程池,具體子線程維護在NioEventLoop中,並且在上篇文章服務端啓動分析中我們也知道了eventLoop是在channel註冊的時候和channel綁定了,接下來我們來看看NioEventLoop的線程到底是什麼時候啓動的。看SingleThreadEventEexecutor代碼時,很容易就能注意到startThread()這個方法,從方法名就知道,該方法就是啓動線程的方法。

private void startThread() {
        if (state == ST_NOT_STARTED) {
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                doStartThread();
            }
        }
    }

    private void doStartThread() {
        assert thread == null;
        executor.execute(new Runnable() {
            @Override
            public void run() {
                thread = Thread.currentThread();
                if (interrupted) {
                    thread.interrupt();
                }
                try {
                    SingleThreadEventExecutor.this.run();
                    success = true;
                } catch (Throwable t) {
                    logger.warn("Unexpected exception from an event executor: ", t);
                    }
        });
    }
//刪除無關代碼

可以看到SingleThreadEventExecutor內部維護了線程狀態state,如果線程還沒啓動,那麼先CAS把狀態修改爲啓動狀態,接着調用doStartThread方法,在doStartThread方法中通過子類傳上來的executor對象調用execute方法,而這個executor對象其實是ThreadPreTaskExecutor類,execute方法則是通過threadFactory創建線程並啓動線程。

public final class ThreadPerTaskExecutor implements Executor {
    private final ThreadFactory threadFactory;

    public ThreadPerTaskExecutor(ThreadFactory threadFactory) {
        if (threadFactory == null) {
            throw new NullPointerException("threadFactory");
        }
        this.threadFactory = threadFactory;
    }

    @Override
    public void execute(Runnable command) {
        threadFactory.newThread(command).start();
    }
}

接着回到上面SingleThreadEventExecutor的doStartThread方法,線程啓動後把當前的線程賦值給thread這個全局變量,接着調用SingleThreadEventExecutor的run方法,而這個方法不出意外是個抽象方法,其具體實現在子類NioEventLoop中

  @Override
    protected void run() {
        for (;;) {
            try {
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));
                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                    default:
                        // fallthrough
                }

                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                if (ioRatio == 100) {
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        runAllTasks();
                    }
                } else {
                    final long ioStartTime = System.nanoTime();
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        final long ioTime = System.nanoTime() - ioStartTime;
                        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
    }

看到這個run方法,更加證實了之前我們說的eventLoop與selector綁定,通過一個忙循環監聽I/O事件並處理的邏輯。那麼到現在我們就只剩一個問題了,什麼時候調用了這個startThread方法?要解答這個問題,我們還是回顧下Channel註冊到Selector上的過程吧。

AbstractBootstrap.initAndRegister() 
	->MultithreadEventLoopGroup.register(Channel channel)
		->SingleThreadEventLoop.register(Channel channel)
			->SingleThreadEventLoop.register(ChannelPromise promise)
				->AbstractChannel.AbstractUnsafe.register(EventLoop eventLoop,ChannelPromise promise) 

在ServerBootstrap.bind的時候,會先初始化Channel和把Channel註冊到Selector上,整個調用鏈如上所示,最終調用的是Unsafe的註冊方法,unsafe的註冊方法如下

@Override
        public final void register(EventLoop eventLoop, final ChannelPromise promise) {
           //忽略無關代碼
            AbstractChannel.this.eventLoop = eventLoop;

            if (eventLoop.inEventLoop()) {
                register0(promise);
            } else {
                try {
                    eventLoop.execute(new Runnable() {
                        @Override
                        public void run() {
                            register0(promise);
                        }
                    });
                } catch (Throwable t) {
                    logger.warn(
                            "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                            AbstractChannel.this, t);
                    closeForcibly();
                    closeFuture.setClosed();
                    safeSetFailure(promise, t);
                }
            }
        }

在這裏會先把eventLoop綁定到channel上,然後調用eventLoop.inEventLoop()方法判斷當前的線程是否就是該eventLoop持有的線程,因爲是在初始化階段,eventLoop的線程都還沒啓動,這裏eventLoop.inEventLoop()返回當然是false,所以執行else階段的代碼,eventLoop.execute(new Runnable(){})這個execute是個接口方法,真正實現在SingleThreadEventExecutor類裏面。

@Override
    public void execute(Runnable task) {
        if (task == null) {
            throw new NullPointerException("task");
        }

        boolean inEventLoop = inEventLoop();
        if (inEventLoop) {
            addTask(task);
        } else {
            startThread();
            addTask(task);
            if (isShutdown() && removeTask(task)) {
                reject();
            }
        }

        if (!addTaskWakesUp && wakesUpForTask(task)) {
            wakeup(inEventLoop);
        }
    }

同樣,這個execute方法先判斷是否爲eventLoop自己持有的線程在執行,在這當然是false,然後就來到了else部分,在這會執行兩個方法startThread()和addTask(task),還記得上面分析的startThread()麼,這就串起來了,通過提交註冊channel到selecot上的任務,觸發線程的啓動。啓動之後把剛剛提交的註冊任務添加到TaskQueue中,這個註冊任務非I/O處理任務,所以提交到TaskQueue中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章