netty源码分析(三):NioEventLoop

193 阅读8分钟

概述:本文将详细讲述netty中的EventLoop,会分为下面十部分:

  • 一. NioEventLoop概述
  • 二. NioEventLoop创建概述
  • 三. ThreadPerTaskThread
  • 四. 创建NioEventLoop线程
  • 五. 创建线程选择器
  • 六. NioEventLoop的启动
  • 七. NioEventLoop执行概述
  • 八. 检测IO事件
  • 九.处理IO事件
  • 十. reactor线程任务的执行

分析之前,还是看netty的启动模板代码 public class MyServer {

public static void main(String[] args) {
    // 1.构建 bossGroup 和 workGroup
    NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
    EventLoopGroup workGroup = new NioEventLoopGroup(1);

    ServerBootstrap serverBootstrap = new ServerBootstrap();
    // 2.设置group
    serverBootstrap.group(bossGroup, workGroup);
    // 3.设置服务端option和attr
    serverBootstrap.option(ChannelOption.SO_BACKLOG, 1);
    serverBootstrap.attr(AttributeKey.valueOf("name"),"彭青松");
    // 4.设置服务端的channel
    serverBootstrap.channel(NioServerSocketChannel.class);
    // 5.设置服务端的handler
    serverBootstrap.handler(new MyServerHandler());
    // 6.设置客户端的handler
    serverBootstrap.childHandler(new ChannelInitializer() {
        @Override
        protected void initChannel(Channel ch) throws Exception {
            // 7.客户端channel初始化的时候,调用这里
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast(new MyTcpClinetHandler());
        }
    });

    try {
        // 8.服务绑定端口,启动
        ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
        channelFuture.channel().closeFuture().sync();
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        bossGroup.shutdownGracefully();
        workGroup.shutdownGracefully();
    }
}

}

一. NioEventLoop概述

从上面代码中可以看到,netty启动的第一步,就是设置两个bossGroup和workGroup, 每一个NioEventLoopGroup包含一个/多个NioEventLoop,每一个NioEventLoop是一个线程,NioEventLoop中包含一个Selector,channel创建后会把自己注册到selector上,并设置关注的事件,然后启动这个NioEventLoop,读取IO事件,处理IO事件,处理任务队列中的任务,如下图:

image.png

二. NioEventLoop创建

NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);

紧着进入

public NioEventLoopGroup(int nThreads) {
    this(nThreads, (Executor) null);
}

接下来会进入几个重载方法,最后进入

protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
    super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}

这里会判断nThreads是否为0,如果在创建NioEventLoop时,不传递大小,默认会创建 cpu个数*2个数量. 继续进入

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                        EventExecutorChooserFactory chooserFactory, Object... args) {
     //...省略一些代码
 
    if (executor == null) {
       //1.创建ThreadPerTaskThread
        executor = new  ThreadPerTaskExecutor(newDefaultThreadFactory());
    }
 
    children = new EventExecutor[nThreads];

    for (int i = 0; i < nThreads; i ++) {
        boolean success = false;
        try {
        //2.创建NioEventLoop
            children[i] = newChild(executor, args);
            success = true;
        } catch (Exception e) {
      
        } finally {
            if (!success) {
                for (int j = 0; j < i; j ++) {
                    children[j].shutdownGracefully();
                }

                for (int j = 0; j < i; j ++) {
                    EventExecutor e = children[j];
                    try {
                        while (!e.isTerminated()) {
                            e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
                        }
                    } catch (InterruptedException interrupted) {
                        // Let the caller handle the interruption.
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }
        }
    }
    // 3.创建线程选择器
    chooser = chooserFactory.newChooser(children);

    final FutureListener<Object> terminationListener = new FutureListener<Object>() {
        @Override
        public void operationComplete(Future<Object> future) throws Exception {
            if (terminatedChildren.incrementAndGet() == children.length) {
                terminationFuture.setSuccess(null);
            }
        }
    };

    for (EventExecutor e: children) {
        e.terminationFuture().addListener(terminationListener);
    }

    Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
    Collections.addAll(childrenSet, children);
    readonlyChildren = Collections.unmodifiableSet(childrenSet);
}

上面代码主要干三件是

2.1. ThreadPerTaskThread

executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());

2.2. 创建NioEventLoop线程

首先根据数据大小,创建NioEventLoop队列

children = new EventExecutor[nThreads];

接着为队列中每一个元素创建一个NioEventLoop

children[i] = newChild(executor, args);

会进入

@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的构造方法

NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
             SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
     //1.创建TaskQueue
    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;
    //2.创建selector
    selector = openSelector();
    selectStrategy = strategy;
}

这里会做几件事情

2.2.1.创建TaskQueue

protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
    return new LinkedBlockingQueue<Runnable>(maxPendingTasks);
}

就是创建一个阻塞队列

2.2.2.创建selector

调用jdk底层

selector = provider.openSelector();

后面会有一个优化,把jdk原生的selectedKeys,是一个List,改为一个组数,因为数组的查找是O(1),这个感兴趣的可以自己看一下,比较简单

2.3. 创建线程选择器

chooser = chooserFactory.newChooser(children);

点进去之后,会进入

@Override
public EventExecutorChooser newChooser(EventExecutor[] executors) {
    if (isPowerOfTwo(executors.length)) {
        return new PowerOfTowEventExecutorChooser(executors);
    } else {
        return new GenericEventExecutorChooser(executors);
    }
}

上图可以看到,如果数组队列大小是2的幂次方,就创建PowerOfTowEventExecutorChooser,否则,GenericEventExecutorChooser选择器

这里线程选择器也是一个Netty的优化,比如我们的workGroup的大小是4,当bossGroup监测到一个新的连接后,会把这个新连接放入workGroup,而workGroup里面有四个对象,怎么从这四个对象,就要使用线程选择器,简要概括

  • PowerOfTowEventExecutorChooser

使用&运算法,轮训获取下一个.[与运算效率更高]

@Override
public EventExecutor next() {
    return executors[idx.getAndIncrement() & executors.length - 1];
}
  • GenericEventExecutorChooser

使用 索引+1 ,在对数组大小取模,求出下一个对象

@Override
public EventExecutor next() {
   return executors[Math.abs(idx.getAndIncrement() % executors.length)];
}

三. NioEventLoop的启动

到这里,NioEventLoop已经创建好了,那他是什么时候启动的呢?

答案:NioEventLoop第一次添加任务的时候,会判断如果是由外部线程调用的话,就开启这个NioEventLoop

启动的入口

ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();

image.png 在doBind()方法中,创建和初始化channel在上一遍文章中已经介绍了源码2 紧着进入doBind0()方法中

private static void doBind0(
        final ChannelFuture regFuture, final Channel channel,
        final SocketAddress localAddress, final ChannelPromise promise) {

    // This method is invoked before channelRegistered() is triggered.  Give user handlers a chance to set up
    // the pipeline in its channelRegistered() implementation.
    channel.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            if (regFuture.isSuccess()) {
                channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            } else {
                promise.setFailure(regFuture.cause());
            }
        }
    });
}

上图可以看到,在一个NioEventLoop在添加任务[执行execute(xxx)]方法时候,会判断,如果是外部线程添加的,就开启这个线程,继续进入 channel.eventLoop().execute(xxx)

[SingleThreadEventExecutor]
@Override
public void execute(Runnable task) {
    if (task == null) {
        throw new NullPointerException("task");
    }
    // 1.判断是否在EventLoop线程
    boolean inEventLoop = inEventLoop();
    if (inEventLoop) {
        addTask(task);
    } else {
      // 2.启动eventLoop线程
        startThread();
        //3.把这个任务添加到eventLoop线程的taskqueue中
        addTask(task);
        if (isShutdown() && removeTask(task)) {
            reject();
        }
    }

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

分析上图三个步骤

3.1 inEventLoop()

image.png 这个方法比较简单,就是判断当前线程和eventLoop中的线程是否是同一个, 因为我们启动的是main线程,所以返回是false,说明eventLoop中的线程还没有启动

3.2 startThread(task)

就是启动这个线程

3.3 addTask();

将这个任务包装下,放入eventLoop中的taskQueue中,这个后面会详细讲解

四. NioEventLoop执行

通过上面的流程,NioEventLoop创建好,并且绑定了一个selector,#netty源码分析(二):服务端启动中,我们已经知道这个selector已经注册了服务端的NioServerSocketChannel,随着NioEventLoop的启动,接下来就是要:监听连接事件-->处理连接事件-->执行线程任务了 代码都在 io.netty.channel.nio.NioEventLoop#run()方法中

protected void run() {
    for (;;) {
        try {
                    continue;
                case SelectStrategy.SELECT:
                   //  1.调用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 {
                 //  2.处理IO事件
                    processSelectedKeys();
                } finally {
                    // Ensure we always run tasks.
                    final long ioTime = System.nanoTime() - ioStartTime;
                     //  3.处理线程池任务
                    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                }
            }
        } catch (Throwable t) {
            handleLoopException(t);
        }
        // Always handle shutdown even if the loop processing threw an exception.
        try {
            if (isShuttingDown()) {
                closeAll();
                if (confirmShutdown()) {
                    return;
                }
            }
        } catch (Throwable t) {
            handleLoopException(t);
        }
    }
}

逻辑如图image.png 下面我们具体讲解

4.1. 检测IO事件

调用select()方法

private void select(boolean oldWakenUp) throws IOException {
    Selector selector = this.selector;
    try {
        int selectCnt = 0;
        long currentTimeNanos = System.nanoTime();
        long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
        for (;;) {
            long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
            if (timeoutMillis <= 0) {
                if (selectCnt == 0) {
                    selector.selectNow();
                    selectCnt = 1;
                }
                break;
            }

            // If a task was submitted when wakenUp value was true, the task didn't get a chance to call
            // Selector#wakeup. So we need to check task queue again before executing select operation.
            // If we don't, the task might be pended until select operation was timed out.
            // It might be pended until idle timeout if IdleStateHandler existed in pipeline.
            if (hasTasks() && wakenUp.compareAndSet(false, true)) {
                selector.selectNow();
                selectCnt = 1;
                break;
            }

            int selectedKeys = selector.select(timeoutMillis);
            selectCnt ++;

            if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                // - Selected something,
                // - waken up by user, or
                // - the task queue has a pending task.
                // - a scheduled task is ready for processing
                break;
            }
            if (Thread.interrupted()) {
                // Thread was interrupted so reset selected keys and break so we not run into a busy loop.
                // As this is most likely a bug in the handler of the user or it's client library we will
                // also log it.
                //
                // See https://github.com/netty/netty/issues/2426
                if (logger.isDebugEnabled()) {
                    logger.debug("Selector.select() returned prematurely because " +
                            "Thread.currentThread().interrupt() was called. Use " +
                            "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
                }
                selectCnt = 1;
                break;
            }

            long time = System.nanoTime();
            if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                // timeoutMillis elapsed without anything selected.
                selectCnt = 1;
            } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                    selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                // The selector returned prematurely many times in a row.
                // Rebuild the selector to work around the problem.
                logger.warn(
                        "Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",
                        selectCnt, selector);

                rebuildSelector();
                selector = this.selector;

                // Select again to populate selectedKeys.
                selector.selectNow();
                selectCnt = 1;
                break;
            }

            currentTimeNanos = time;
        }

        if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
            if (logger.isDebugEnabled()) {
                logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                        selectCnt - 1, selector);
            }
        }
    } catch (CancelledKeyException e) {
        if (logger.isDebugEnabled()) {
            logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                    selector, e);
        }
        // Harmless exception - log anyway
    }
}

这里netty对selector空轮询进行了处理, 空轮询bug后面会在写一篇文章详细讲解

4.2.处理IO事件

selector.select()监测到IO事件后,就要进行处理了,代码是

processSelectedKeys();

紧着进入

private void processSelectedKeys() {
    if (selectedKeys != null) {
        processSelectedKeysOptimized(selectedKeys.flip());
    } else {
        processSelectedKeysPlain(selector.selectedKeys());
    }
}

接着接入 NioEventLoop#processSelectedKeysOptimized方法

image.png

image.png

unsafe类是处理channel中的数据,主要包括数据到读写,在连接过程中,会把新的连接读取到,选择workGroup中的一个EventLoop进行注册到selector上,该selector后续可以处理这个客户端的读写请求,后面会在写文章详细讲解 unsafe类

4.3. reactor线程任务的执行

处理完IO事件后,就要处理线程任务了,根据用户设置的IoRatio参数,设置"处理IO事件"和"执行线程"的时间比例,IoRatio可以在NioEventlLoop设置,如果不设置,默认是50

NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
bossGroup.setIoRatio(20);

首先根据ioRate比例,算出来执行线程任务要消耗多少时间,然后执行线程任务

final long ioStartTime = System.nanoTime();
try {
    processSelectedKeys();
} finally {
    // Ensure we always run tasks.
    final long ioTime = System.nanoTime() - ioStartTime;
    //ioTime * (100 - ioRatio) / ioRatio 就就是算出时间
    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}

接着执行线程任务

protected boolean runAllTasks(long timeoutNanos) {
    fetchFromScheduledTaskQueue();
    Runnable task = pollTask();
    if (task == null) {
        afterRunningAllTasks();
        return false;
    }

    final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
    long runTasks = 0;
    long lastExecutionTime;
    for (;;) {
        safeExecute(task);

        runTasks ++;

        // Check timeout every 64 tasks because nanoTime() is relatively expensive.
        // XXX: Hard-coded value - will make it configurable if it is really a problem.
        if ((runTasks & 0x3F) == 0) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            if (lastExecutionTime >= deadline) {
                break;
            }
        }

        task = pollTask();
        if (task == null) {
            lastExecutionTime = ScheduledFutureTask.nanoTime();
            break;
        }
    }

    afterRunningAllTasks();
    this.lastExecutionTime = lastExecutionTime;
    return true;
}

上图核心逻辑,就是

  • 1.从taskqueue中取出任务
  • 2.执行线程 [safeExecute(task)],注意这里是执行调用线程的run方法,是串行阻塞调用
  • 3.继续从taskqueue中取出下一个任务执行,执行64次个任务后,判断一下时间是否过期,过期的话,就停止执行

五. NioEventLoop总结

NioEventLoop是一个线程,里面有一个selector,服务端的selector关注的是连接请求,当有新的连接接入时候,会创建NioSocketChannel并注册到workGroup的一个NioEventLoop上,NioEventLoop启动后,会循环做三件事情:

  • 检测IO事件
  • 处理IO事件
  • 执行线程任务