Netty source code analysis 6:NioEventLoop

Posted by donbre on Sun, 06 Mar 2022 19:36:53 +0100

Basic introduction to NioEventLoop

When building the Netty service, you first need to create a NioEventLoopGroup, that is, the following code:

NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
  • Initialization timing: NioEventLoopGroup defines an EventExecutor[] children member variable (NioEventLoop is a subclass of EventExecutor). Children will also be initialized when NioEventLoopGroup is initialized. The relationship between NioEventLoopGroup and NioEventLoop is somewhat similar to that between thread pool and thread, and NioEventLoop itself has the characteristics of thread pool, Therefore, the relationship between them is similar to that of nested thread pools in thread pools.
  • Main function: the thread associated with NioEventLoop will call selector circularly The select () method listens to events on relevant channels. In addition, it will execute externally submitted tasks based on certain policies. The relevant channels are distributed by the boss based on polling, which ensures the load balance between nioeventloops. External tasks mainly include writing data to the Channel and user-defined tasks submitted by users
  • In the above code, boss defines a NioEventLoop, because boss is usually used to receive client connection requests. When there is a bottleneck in processing link requests, you can create more nioeventloops for the boss based on multi port listening; By default, the worker will create: the number of processor cores * 2 nioeventloops. After the boss obtains the client connection request, he will register the established Channel with a NioEventLoop in the worker, and the NioEventLoop will process the IO on the Channel.
  • The class diagram structure of NioEventLoop is as follows:
    • The ScheduledExecutorService that implements jdk has the characteristics of thread pool. The following is a brief description of the functions of each interface
    • EventExecutorGroup: implements this interface, which indicates that when we call the method on EventExecutorGroup, we actually call the corresponding method of the internal EventExecutor based on the polling method. This interface provides relevant methods to gracefully close EventExecutor,
      • next(): polling internal EventExecutor
      • Shutdown gracefully(): gracefully close the current EventExecutorGroup, that is, gracefully close the internal EventExecutor
      • terminationFuture(): get the asynchronous end result. When the EventExecutorGroup is closed, the result will be returned. Before that, the result will be blocked
      • isShuttingDown(): Returns whether closing is complete
      • Other methods inherit from the interface of the jdk interface, only change the return value and return the advanced asynchronous result defined by Netty (inherited from the future of the jdk)
    • EventExecutor: used to determine whether a thread is an internal thread associated with EventExecutor. In order to avoid multithreading problems, calls to the relevant configuration methods of EventExecutor and its subclass (NioEventLoop) are usually submitted to NioEventLoop based on the task situation for its associated thread to execute. For example, when an external thread registers a Channel with NioEventLoop, it does not directly call the relevant methods of NioEventLoop, but encapsulates the task and submits it to NioEventLoop, Judge by the method provided by EventExecutor. In addition, it also provides asynchronous results of successful and failed rapid construction.
      • inEventLoop(): judge whether the current thread belongs to the internal thread associated with EventExecutor
      • next(): returns the current EventExecutor fixedly
      • parent(): returns the corresponding EventLoopGroup
      • Other methods are used to create asynchronous results
    • EventLoopGroup: similar to EventExecutorGroup, EventExecutorGroup provides methods related to submitting tasks, while EventLoopGroup provides methods to register channels. In addition, it also provides a method for polling internal eventexecutors
      • next(): polling internal EventExecutor
      • register(): register the Channel to the EventExecutor returned by next() (based on task submission)
    • OrderedEventExecutor: the tag interface has no method to process the submitted task in an orderly serial manner.
    • EventLoop: the event loop actually just overrides the Fred group() method to return a more specific subclass EventLoopGroup

NioEventLoop initialization process

When initializing NioEventLoopGroup, NioEventLoop#newChild() method will be called to create NioEventLoop.

	//newChild method of NioEventLoopGroup class
    protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        SelectorProvider selectorProvider = (SelectorProvider) args[0];
        SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1];
        RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2];
        EventLoopTaskQueueFactory taskQueueFactory = null;
        EventLoopTaskQueueFactory tailTaskQueueFactory = null;

        int argsLength = args.length;
        if (argsLength > 3) {
            taskQueueFactory = (EventLoopTaskQueueFactory) args[3];
        if (argsLength > 4) {
            tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4];
        return new NioEventLoop(this, executor, selectorProvider,
                rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory);
  • Specific references are as follows:
    • this:NioEventLoopGroup instance
    • executor:ThreadPerTaskExecutor
    • selectorProvider: the selectorProvider multiplexer provider of the jdk
    • selectStrategyFactory.newSelectStrategy(): DefaultSelectStrategy, the default selection strategy, which determines whether NioEventLoop performs external submission tasks or listens to Channel events
    • rejectedExecutionHandler: when a bounded queue is used to temporarily stage a task, if the queue is full, the processor is used to decide how to handle it, and an exception is thrown by default
    • taskQueueFactory: task queue and tasks submitted through execute/submit will be saved to this queue
    • tailTaskQueueFactory: Netty has not been used yet. It is designed to define a task queue with lower priority than taskQueueFactory
  • NioEventLoop internal member variable and static code block description
    • DEFAULT_MAX_PENDING_TASKS: the maximum value of the task queue. The default value is integer MAX_ VALUE
    • Wake = - 1, works together with nextWakeupNanos. When the value of nextWakeupNanos is wake, it will pass the selector Wakeup wakes up the thread from the blocking listening event to execute the external submission task. In addition, when wake = - 1 indicates that the thread is running, the externally submitted scheduled task will be directly put into the queue, and the wake-up task will not be added to wake up the thread. NONE corresponds to it, and the select() operation will not set the timeout
    • Canceledkeys: when the Channel is closed, the deregister event will be triggered when the opposite end closes the Channel, and finally the Channel will be deleted from the selector. This value counts the times of deletion on the selector. When it reaches cleanup_ When interval = 256, the selector will be rebuilt from the starting selector. Note: deregistration is performed in the form of submitting tasks, so the accumulation of this value is only possible when a large number of channels are cancelled in a short time, because all tasks will be reset to 0 when they are completed and executed next time
    • DISABLE_KEY_SET_OPTIMIZATION: disable the optimization key function. The default is false. The selectedKeys and publicSelectedKeys types inside the Selector will be replaced with the SelectedSelectionKeySet set that can be expanded
    • Ioratio: proportion of IO time, 50% by default
    • MIN_PREMATURE_SELECTOR_RETURNS=3. If the number of empty polling is exceeded, the multiplexer will be rebuilt. The default is 512. When the custom is lower than 3, it is 3
    • needsToSelectAgain: before processing the io event, you need to execute selectNow() again to retrieve the event. It is true when the channel is closed. It is only useful during the event processing, and the loop body will not be empty
    • provider: SelectorProvider
    • selectedKeys: SelectedSelectionKeySet, that is, the optimized keyset
    • selectNowSupplier: the implementation of selectNow(), which is jdk's selectNow() by default. In addition, there are corresponding implementations in window/linux
    • Selector: selector, which internally uses SelectedSelectionKeySet instead of the original HashSet
    • unwrappedSelector: the Selector uses the original HashSet internally

Instantiation source code analysis

  • The constructor has no complex initialization logic and is a simple assignment operation
    NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
                 SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
                 EventLoopTaskQueueFactory taskQueueFactory, EventLoopTaskQueueFactory tailTaskQueueFactory) {
                 //newTaskQueue() will use MpscUnboundedArrayQueue to cache the task queue, which is more efficient than BlockingQueue
                 //Continue calling the parent constructor
        super(parent, executor, false, newTaskQueue(taskQueueFactory), newTaskQueue(tailTaskQueueFactory),
        this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider");
        this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy");
        //Create selector multiplexer
        final SelectorTuple selectorTuple = openSelector();
        this.selector = selectorTuple.selector;//Key set optimized multiplexer
        this.unwrappedSelector = selectorTuple.unwrappedSelector;//Original multiplexer
	//The parent class constructor, addTaskWakesUp=false, indicates that external tasks will be added through the selector Wakeup wakeup blocking listening
    protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor,
                                    boolean addTaskWakesUp, Queue<Runnable> taskQueue, Queue<Runnable> tailTaskQueue,
                                    RejectedExecutionHandler rejectedExecutionHandler) {
        //Continue calling the parent constructor                            
        super(parent, executor, addTaskWakesUp, taskQueue, rejectedExecutionHandler);
        tailTasks = ObjectUtil.checkNotNull(tailTaskQueue, "tailTaskQueue");

    protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
                                        boolean addTaskWakesUp, Queue<Runnable> taskQueue,
                                        RejectedExecutionHandler rejectedHandler) {
        super(parent);//Simply assign parent to the member variable in the parent class constructor
        this.addTaskWakesUp = addTaskWakesUp;
        this.maxPendingTasks = DEFAULT_MAX_PENDING_EXECUTOR_TASKS;
        //The passed in executor here is the threadpertask executor passed in from NioEventLoopGroup through threadexectormap Apple did the encapsulation
        this.executor = ThreadExecutorMap.apply(executor, this);
        this.taskQueue = ObjectUtil.checkNotNull(taskQueue, "taskQueue");
        this.rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
  • ThreadExecutorMap.apply(executor, this): this method call encapsulates the incoming threadpertask executor, as follows:
    public static Executor apply(final Executor executor, final EventExecutor eventExecutor) {
        ObjectUtil.checkNotNull(executor, "executor");
        ObjectUtil.checkNotNull(eventExecutor, "eventExecutor");
        //Return a new Executor, and execute tasks internally through threadpertask Executor,
        //The submitted task is simply encapsulated by the apply() method
        return new Executor() {
            public void execute(final Runnable command) {
                executor.execute(apply(command, eventExecutor));
	//Encapsulate the submitted task and add additional logic at the beginning and end of the original task
	//The purpose is to obtain the NioEventLoop associated with the thread within the task
    public static Runnable apply(final Runnable command, final EventExecutor eventExecutor) {
        ObjectUtil.checkNotNull(command, "command");
        ObjectUtil.checkNotNull(eventExecutor, "eventExecutor");
        return new Runnable() {
            public void run() {
            	//Put the current NioEventLoop into the local thread variable before the task starts
                try {
                } finally {
            		//Removes the NioEventLoop variable from the current thread before ending the task

How does NioEventLoop work

In the server programming of Netty, by calling the ServerBootstrap#bind() method, a registration task will be submitted internally to the boss(NioEventLoop). At this time, NioEventLoop will be triggered to start a thread to work. Therefore, the analysis will start from NioEventLoop#execute(). The execute() method is implemented by the parent SingleThreadEventExecutor. The source code is as follows:


    public void execute(Runnable task) {
        ObjectUtil.checkNotNull(task, "task");
        //Method overloading
        execute(task, !(task instanceof LazyRunnable) && wakesUpForTask(task));
    private void execute(Runnable task, boolean immediate) {
    	//Judge whether the current thread is a NioEventLoop associated thread. The submitted registration task belongs to the main thread, so it is not false
        boolean inEventLoop = inEventLoop();
        //Task team
        if (!inEventLoop) {
        	//Start thread
            if (isShutdown()) {
                boolean reject = false;
                try {
                    if (removeTask(task)) {
                        reject = true;
                } catch (UnsupportedOperationException e) {
                    // The task queue does not support removal so the best thing we can do is to just move on and
                    // hope we will be able to pick-up the task before its completely terminated.
                    // In worst case we will log on termination.
                if (reject) {
		//addTaskWakesUp = false (constructor), immediate = true;
        if (!addTaskWakesUp && immediate) {
        	//Wake up blocking listening

inEventLoop(): judge whether the current thread is a NioEventLoop associated thread

When submitting the first task, the thread has not been started. At this time, it is null, so the judgment is false

    public boolean inEventLoop() {
        return inEventLoop(Thread.currentThread());
    public boolean inEventLoop(Thread thread) {
        return thread == this.thread;

addTask(): task queued to be executed

    protected void addTask(Runnable task) {
        ObjectUtil.checkNotNull(task, "task");
        //Queue failure, execution rejection logic, and constructors such as the disastrous rejectedExecutionHandler throw exceptions by default
        if (!offerTask(task)) {

startThread(): start NioEventLoop associated thread

    private void startThread() {
        if (state == ST_NOT_STARTED) {
        	//cas sets the state of NioEventLoop from not started to started
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                boolean success = false;
                try {
                    success = true;
                } finally {
                    if (!success) {
                    	//If startup fails, restore the state to not started
                        STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED);

    private void doStartThread() {
        assert thread == null;
        //The executor here belongs to threadpertask executor, and its execute() method is shown below, which will start a
        //FastThreadLocalThread thread thread to execute the task (FastThreadLocalThread inherits Thread)
        executor.execute(new Runnable() {
            public void run() {
            	//Assign the current thread to the associated thread of NioEventLoop
                thread = Thread.currentThread();
                if (interrupted) {

                boolean success = false;
                //Record the time interval of the thread since the service was started
                try {
                	//That is, execute NioEventLoop#run(), which is a wireless loop and knows that the external sends a close command
                    success = true;
                } catch (Throwable t) {
                    logger.warn("Unexpected exception from an event executor: ", t);
                } finally {
                //···Omit the shutdown logic

NioEventLoop#run(): loop blocking listens to Channel events and executes external submission tasks

So far, the NioEventLoop associated thread has been started. Let's analyze the run method

    protected void run() {
        int selectCnt = 0;
        for (;;) {
            try {
                int strategy;
                try {
                	//Calculate the selection policy. If there is a task, it is determined by the value provided by selectNowSupplier. Otherwise, perform blocking listening
                	//selectNowSupplier will execute selectnow() > = 0
                	//There must be a task in the first loop, so the select() block will be skipped to execute the task
                	//Strategy > = 0 or = - 1 (blocking listening case)
                    strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
                    switch (strategy) {
                    case SelectStrategy.CONTINUE://-2 impossible

                    case SelectStrategy.BUSY_WAIT://-3 impossible
                        // fall-through to SELECT since the busy-wait is not supported with NIO

                    case SelectStrategy.SELECT://This branch will be taken when the task is cycled again after execution
                    	//There is no scheduled task, curDeadlineNanos = -1
                        long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
                        if (curDeadlineNanos == -1L) {
                            curDeadlineNanos = NONE; // Non timeout blocking until the event arrives
                        //After unlimited blocking is set here, you will try to wake up when raising the price schedule task
                        try {
                            if (!hasTasks()) {//Finally, block listening after detecting whether there are tasks
                                strategy = select(curDeadlineNanos);
                        } finally {
                            // It means that the task is currently executing, so there is no need to wake up when the external task arrives
                        // fall through
                } catch (IOException e) {
                    // If we receive an IOException here its because the Selector is messed up. Let's rebuild
                    // the selector and retry.
                    selectCnt = 0;

                selectCnt++;//Blocking monitoring times + 1;
                cancelledKeys = 0;//Reset channel closing cumulative value
                needsToSelectAgain = false;//selectNow() needs to be reset again
                final int ioRatio = this.ioRatio;
                boolean ranTasks;
                if (ioRatio == 100) {
                    try {
               			 //When io accounts for 100%, if there is a Channel event, process the event
                        if (strategy > 0) {
                    } finally {
                        // Perform all tasks
                        ranTasks = runAllTasks();
                } else if (strategy > 0) {
                    final long ioStartTime = System.nanoTime();
                    try {
                    	//Handling io events
                    } finally {
                        // Calculate io time
                        final long ioTime = System.nanoTime() - ioStartTime;
                        //Calculate the time when the task should be executed according to ioRatio
                        ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                } else {
                	//If the execution time is 0, the maximum number of tasks will be 64, and subsequent tasks will not be executed after that
                    ranTasks = runAllTasks(0);
				//selectCnt resets whenever a task or io event is executed in each loop
                if (ranTasks || strategy > 0) {
                	//Reset selectCnt if there are no more empty polls after more than 3 empty polls
                    if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
                        logger.debug(" returned prematurely {} times in a row for Selector {}.",
                                selectCnt - 1, selector);
                    selectCnt = 0;//Reset
                  //If NULL polling is caused by interrupted wake-up, reset to zero and reset selectCnt
                  //Otherwise, if the empty polling exceeds 512 times, it is considered that a jdk Selector empty polling bug is generated. Rebuild the Selector and recharge selectCnt
                } else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
                    selectCnt = 0;
            } catch (CancelledKeyException e) {
                // Harmless exception - log anyway
                if (logger.isDebugEnabled()) {
                    logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                            selector, e);
            } catch (Error e) {
                throw e;
            } catch (Throwable t) {
            } finally {
                // Always handle shutdown even if the loop processing threw an exception.
                try {
                    if (isShuttingDown()) {
                        if (confirmShutdown()) {
                } catch (Error e) {
                    throw e;
                } catch (Throwable t) {

processSelectedKeys(): traverse Channel ready events

If a large number of Channel shutdown events (more than 256) occur during traversal, the unhandled Channel events are cleared and retrieved from the new selectNow()

    private void processSelectedKeys() {
        if (selectedKeys != null) {
            processSelectedKeysOptimized();//select() optimization is enabled by default, and this logic is followed
        } else {
    private void processSelectedKeysOptimized() {
    	//Traverse each SelectedKey
        for (int i = 0; i < selectedKeys.size; ++i) {
            final SelectionKey k = selectedKeys.keys[i];
            // null out entry in the array to allow to have it GC'ed once the Channel close
            // See
            selectedKeys.keys[i] = null;

            final Object a = k.attachment();
			//selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
			//AbstractNioChannel is attached when registering
            if (a instanceof AbstractNioChannel) {
            	//Handling channel ready events
                processSelectedKey(k, (AbstractNioChannel) a);
            } else {
            	//If NioTask is attached to the registration, AbstractNioChannel cannot be obtained later
            	//Only the content defined by task can be executed in a fixed way. At present, no specific implementation class is found
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                processSelectedKey(k, task);
			//If many (more than 256)Channel close events occur during this cycle, needsToSelectAgain will be set to true
			//Perform selectNow() update event
            if (needsToSelectAgain) {
                // null out entries in the array to allow to have it GC'ed once the Channel close
                // See
                //Clear the unprocessed Channel events and re select now() to prevent
                selectedKeys.reset(i + 1);
				//Reselect now ()
                i = -1;

processSelectedKey(): process specific Channel events. Type: OP_READ,OP_WIRTE_OPCONNECT,OP_ACCEPT

    private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
        if (!k.isValid()) {
        	//Ensure that the channel corresponding to the current event is in a valid state
            final EventLoop eventLoop;
            try {
                eventLoop = ch.eventLoop();
            } catch (Throwable ignored) {
                // If the channel implementation throws an exception because there is no event loop, we ignore this
                // because we are only trying to determine if ch is registered to this event loop and thus has authority
                // to close ch.
                //Ensure that the channel is bound to a specific EventLoop
            // Only close ch if ch is still registered to this EventLoop. ch could have deregistered from the event loop
            // and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is
            // still healthy and should not be closed.
            // See
            if (eventLoop == this) {
                // close the channel if the key is not valid anymore
                //Close Channel

        try {
            int readyOps = k.readyOps();
            // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
            // the NIO JDK channel implementation may throw a NotYetConnectedException.
            //Handle the link event on the Channel. This event only occurs on the client. When the client executes the Channel Listen after connect
            //CONNECT event, after listening to this time, call finishConnect() to complete the connection, otherwise, if connect() directly executes.
            //NotYetConnectedException may be thrown during read or write operations
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                // remove OP_CONNECT as otherwise will always return without blocking
                // See
                int ops = k.interestOps();
                //This event occurs once on this channel, so log off this event on this channel after processing
                ops &= ~SelectionKey.OP_CONNECT;
				//jdk nio's finishConnect() will return a boolean value to determine whether the connection is complete. unsafe encapsulates the operation. If
				//If this is still false, an exception will be thrown

            // Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
            //Handle the write event on the Channel and flush the data on the write buffer defined by Netty to the opposite end
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write

            // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
            // to a spin loop
            //Processing read requests and link requests on the channel. In fact, both read requests and connection requests obtain data from the channel, and the latter only obtains channel data
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
        } catch (CancelledKeyException ignored) {

At this point, the mainstream process of NioEventLoop has ended. Please sort out some methods during the period as follows:

  • Run() - > nextscheduledtaskdeadlinenanos(): use this method to obtain the execution time of the latest scheduled task during select() blocking listening, so as to resume the execution of the scheduled task after blocking listening. If there is no scheduled task, the listening will be blocked directly until a ready event arrives
  • Run() - > runAllTasks(), runAllTasks(long times), runAllTasks(0): used to execute externally submitted tasks. Regardless of the ioRatio ratio iwei, Channel events should be executed first. runAllTasks() will execute all tasks, and runAllTasks(long times) will return after the deadline. In fact, 64 tasks are used as execution units
  • processSelectedKeysOptimized and processSelectedKeysPlain: both are executed by traversing ready events. The difference is that the former needs to execute custom optimization logic because the Selector is optimized

Topics: Java