Netty进阶:Netty核心NioEventLoop原理解析


Netty提供了Java NIO Reactor模型的实现,之前写过一篇文章是对三种Reactor模式的简单实现: Reactor模型的Java NIO实现,当然netty中的实现要复杂的多。并且Netty将实现好的Reactor模型封装起来,我们只需提供适当的参数就可以实现不同线程模式的Reactor模型。

单线程模型

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup)

实例化了一个 NioEventLoopGroup, 构造器参数是1, 表示 NioEventLoopGroup 的线程池大小是1. 然后接着我们调用 b.group(bossGroup) 设置了服务器端的 EventLoopGroup。

多线程模型

EventLoopGroup bossGroup = new NioEventLoopGroup(n);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)

主从多线程模型

EventLoopGroup bossGroup = new NioEventLoopGroup(4);
EventLoopGroup workerGroup = new NioEventLoopGroup(8);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)

Netty的NioEventLoopGroup在 accept 阶段, 不会使用到多线程,也就是说n设置1和10是没有区别的。服务端的 ServerSocketChannel 只绑定到了 bossGroup 中的一个线程, 因此在调用 Java NIO 的 Selector.select 处理客户端的连接请求时, 实际上是在一个线程中的, 所以对只有一个服务的应用来说, bossGroup 设置多个线程是没有什么作用的, 反而还会造成资源浪费。
EpollEventLoopGroup在accept 阶段可以使用多线程来处理连接请求。

1. NioEventLoopGroup

NioEventLoopGroup 类层次结构:
NIOEventLoopGroup
下面分析实例化过程,NioEventLoopGroup 有几个重载的构造器, 不过内容都没有什么大的区别, 最终都是调用的父类MultithreadEventLoopGroup构造器:

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

此时个参数值对应如下:

	nThreads:0
	executor: null
	selectorProvider: SelectorProvider.provider()
	selectStrategyFactory: DefaultSelectStrategyFactory.INSTANCE
	拒绝策略 RejectedExecutionHandlers.reject()

如果我们传入的线程数 nThreads 是0, 那么 Netty 会为我们设置默认的线程数 DEFAULT_EVENT_LOOP_THREADS, 而这个默认的线程数是怎么确定的呢?其实很简单, 在静态代码块中, 会首先确定 DEFAULT_EVENT_LOOP_THREADS 的值:

 static {
        DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
                "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); 
    }

回到MultithreadEventLoopGroup构造器中, 这个构造器会继续调用父类 MultithreadEventExecutorGroup 的构造器:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {
    this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);
}

在此构造方法中,我们指定了一个EventExecutor的选择工厂DefaultEventExecutorChooserFactory厂主要是用于选择下一个可用的EventExecutor, 其内部有两种选择器, 一个是基于位运算,一个是取余。EventExecutor

MultithreadEventExecutorGroup 内部也维护了一个 EventExecutor 数组,每当 Netty 需要一个 EventLoop 时, 会调用EventExecutorChooser的next() 方法获取一个可用的 EventExecutor。

继续回到MultithreadEventExecutorGroup的构造器:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
		 // 初始化executor
		if (executor == null) {
            executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
        }
 		// 初始化EventExecutor
        children = new EventExecutor[nThreads];
        for (int i = 0; i < nThreads; i ++) {
            boolean success = false;
            children[i] = newChild(executor, args);
            success = true;
        }
		// 生成选择器对象
        chooser = chooserFactory.newChooser(children);
    }

此构造方法主要做了三件事:

  1. 初始化executor为ThreadPerTaskExecutor的实例
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();
    }
}

ThreadPerTaskExecutor 实现了Executor接口,其内部会通过newDefaultThreadFactory()指定的默认线程工厂来创建线程,并执行相应的任务。

  1. 创建一个大小为 nThreads的EventExecutor数组children,然后为每一个数组元素创建EventExecutor。children[i] = newChild(executor, args);, newChild(executor, args)方法在MultithreadEventExecutorGroup中没有实现,我们在NioEventLoopGroup中找到了newChild的实现:
@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]);
}
  1. 根据我们默认DefaultEventExecutorChooserFactory选择器工厂,绑定NioEventLoop数组对象。在前面的构造方法中,我们指定了chooserFactory为DefaultEventExecutorChooserFactory,在此工厂内部,会根据children数组的长度来动态选择选择器对象,用于选择下一个可执行的EventExecutor,也就是NioEventLoop。

总结一下整个 NioEventLoopGroup 的初始化过程:

  • NioEventLoopGroup(其实是MultithreadEventExecutorGroup) 内部维护一个类型为 EventExecutor children 数组, 其大小是 nThreads, 这样就构成了一个线程池

  • 如果我们在实例化 NioEventLoopGroup 时, 如果指定线程池大小, 则 nThreads 就是指定的值, 反之是处理器核心数 * 2

  • MultithreadEventExecutorGroup 中会调用 newChild 抽象方法来初始化 children 数组。抽象方法 newChild 是在 NioEventLoopGroup 中实现的, 它返回一个 NioEventLoop 实例.

最后以一张图来总结:
在这里插入图片描述

2. NioEventLoop

NioEventLoop 继承于 SingleThreadEventLoop, 而 SingleThreadEventLoop 又继承于 SingleThreadEventExecutor. SingleThreadEventExecutor 是 Netty 中对本地线程的抽象, 它内部有一个 Thread thread 属性, 存储了一个本地 Java 线程. 因此我们可以认为, 一个 NioEventLoop 其实和一个特定的线程绑定, 并且在其生命周期内, 绑定的线程都不会再改变。
NIOEventLoop
NioEventLoop的父类以及接口比较多,但是只需要关注几个重要的:

  • SingleThreadEventLoop
  • SingleThreadEventExecutor :实现任务队列
  • AbstractScheduledEventExecutor:主要是实现定时任务
2.1实例化过程

在newChild方法中,调用了下面NioEventLoop构造函数:

NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
             SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
    super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler); 
    provider = selectorProvider;//1
    final SelectorTuple selectorTuple = openSelector();//2
    selector = selectorTuple.selector;
    unwrappedSelector = selectorTuple.unwrappedSelector;
    selectStrategy = strategy;//3
}
  1. 在初始化NioEventLoopGroup的时候就已经用到了SelectorProvider这个参数了,SelectorProvider是Java NIO中的抽象类,它的作用是调用Windows或者Linux底层Epoll的实现,比如经常用的Selector.open()方法内部就是通过调用SelectorProvider.openSelector()来得到多路复用器selector。
  2. SelectorTuple是NioEventLoop的内部类,其实就是对Java NIO Selector的封装。
private static final class SelectorTuple {
    final Selector unwrappedSelector;
    final Selector selector;
}

selector和unwrappedSelector分别表示优化过的Selector和未优化过的Selector,selectedKeys表示优化过的SelectionKey。Netty在该类中对Java NIO的Selector做了优化,可以通过设置系统属性io.netty.noKeySetOptimization进行修改,设置为true、yes或者1关闭优化,设置为false、no或者0开启优化,默认开启优化,下面一小节介绍优化的细节。

接下里进入父类SingleThreadEventLoop的构造函数super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler):

protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor,
                                boolean addTaskWakesUp, int maxPendingTasks,
                                RejectedExecutionHandler rejectedExecutionHandler) {
    super(parent, executor, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler);
    tailTasks = newTaskQueue(maxPendingTasks);
}

在此构造函数中只是初始化了TaskQueue,长度默认为DEFAULT_MAX_PENDING_TASKS,该常量定义于SingleThreadEventLoop类中,默认为16。继续看父类SingleThreadEventExecutor的构造函数:

 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");
    }

executor即线程池,还记得上节讲的初始化NioEventLoopGroup么,在MultithreadEventExecutorGroup构造函数中执行executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()),一边保存在类属性中,一边传入了newChild方法中,最终也传入该构造函数。

并且在SingleThreadEventExecutor类中有一个属性private volatile Thread thread,它用来引用支撑该EventExecutor的线程,用来处理I/O事件和执行任务,叫支撑线程或者I/O线程均可,thread所引用的线程即来自executor。

这里也初始化了taskQueue,其中tailTasks和taskQueue均是任务队列,而优先级不同,taskQueue的优先级高于tailTasks和定时任务,定时任务优先级高于tailTasks。所谓的优先级就是线程执行任务的先后。只是tailTasksm目前在Netty中还没有用到。

newTaskQueuef方法被NioEventLoop重写,其实现是Mpsc队列(多个生产者单个消费者的意思),后面会单独的讲述它的原理,而在AbstractScheduledEventExecutor的scheduledTaskQueues是优先级队列。

protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
   return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue()
                                                   : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
}

声明一点:
本文中使用的是Netty4.1.22版本,相比之前的版本,这里对线程模型做了一些小改动。Thread thread属性应用了线程池中的线程,也就是execute中的线程,而在旧版本中是指向独立的线程,并且是通过线程工厂创建的。旧版本如下

protected SingleThreadEventExecutor(
        EventExecutorGroup parent, ThreadFactory threadFactory, boolean addTaskWakesUp) {
    this.parent = parent;
    this.addTaskWakesUp = addTaskWakesUp;

    thread = threadFactory.newThread(new Runnable() {
        @Override
        public void run() {
            boolean success = false;
            updateLastExecutionTime();
            SingleThreadEventExecutor.this.run();
            success = true;
        }
    });
    threadProperties = new DefaultThreadProperties(thread);
    taskQueue = newTaskQueue();
}
2.2 Netty 对Selecter的优化

在NioEventLoop实例化的过程中有提到,Netty对JDK Selector的优化,其实主要是对SelectKeys进行优化,JDK NIO中比如获取准备好的key通过如下代码:

 Set<SelectionKey> keys = selector.selectedKeys();

那么返回的是Set接口的实现HashSet,SeletctorImp中的定义如下。

protected Set<SelectionKey> selectedKeys = new HashSet();
protected HashSet<SelectionKey> keys = new HashSet();

每当向Selector注册时,对自动向key中添加元素,调用select方法后会更新该集合。Netty用SelectedSelectionKeySet实现了AbstractSet,提供了和HashSet同样的方法,只是内部实现(HashSet内部是HashMap)用数组来实现,至于为什么要这样做,本人认为主要以下亮点。

  1. 省去了Map中的value对象的内存,因为动辄百万连接的Netty产生了大量的SelectKey对象,value浪费的内存可想而知。
  2. 更方便的扩容。那么Netty是如何做到的?其实就是在NioEventLoop构造的过程中调用的openSelector方法内部。
private SelectorTuple openSelector() {
        final Selector unwrappedSelector;
        unwrappedSelector = provider.openSelector();
        if (DISABLE_KEYSET_OPTIMIZATION) {// 如果为开启key set 优化,构建普通的SelectorTuple
            return new SelectorTuple(unwrappedSelector);
        }
        // 获取到SelectorImpl的对象,可能失败
        Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                try {
                    return Class.forName(
                            "sun.nio.ch.SelectorImpl",
                            false,
                            PlatformDependent.getSystemClassLoader());
                } catch (Throwable cause) {
                    return cause;
                }
            }
        });

        if (!(maybeSelectorImplClass instanceof Class) ||
            // ensure the current selector implementation is what we can instrument.
            !((Class<?>) maybeSelectorImplClass).isAssignableFrom(unwrappedSelector.getClass())) {
            if (maybeSelectorImplClass instanceof Throwable) {
                Throwable t = (Throwable) maybeSelectorImplClass;
                logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, t);
            }
            return new SelectorTuple(unwrappedSelector);
        }

        final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
        final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
		// 获取到 SelectorImpl的属性 selectedKeys publicSelectedKeys
        Object maybeException = AccessController.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                try {
                    Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
                    Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");

                    if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) {
                        // Let us try to use sun.misc.Unsafe to replace the SelectionKeySet.
                        // This allows us to also do this in Java9+ without any extra flags.
                        long selectedKeysFieldOffset = PlatformDependent.objectFieldOffset(selectedKeysField);
                        long publicSelectedKeysFieldOffset =
                                PlatformDependent.objectFieldOffset(publicSelectedKeysField);

                        if (selectedKeysFieldOffset != -1 && publicSelectedKeysFieldOffset != -1) {
                            PlatformDependent.putObject(
                                    unwrappedSelector, selectedKeysFieldOffset, selectedKeySet);
                            PlatformDependent.putObject(
                                    unwrappedSelector, publicSelectedKeysFieldOffset, selectedKeySet);
                            return null;
                        }
                        // We could not retrieve the offset, lets try reflection as last-resort.
                    }

                    Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
                    if (cause != null) {
                        return cause;
                    }
                    cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
                    if (cause != null) {
                        return cause;
                    }

                    selectedKeysField.set(unwrappedSelector, selectedKeySet);
                    publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
                    return null;
                } catch (NoSuchFieldException e) {
                    return e;
                } catch (IllegalAccessException e) {
                    return e;
                }
            }
        });

        if (maybeException instanceof Exception) {
            selectedKeys = null;
            Exception e = (Exception) maybeException;
            logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, e);
            return new SelectorTuple(unwrappedSelector);
        }
        selectedKeys = selectedKeySet;
        logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector);
        return new SelectorTuple(unwrappedSelector,
                                 new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
    }

先是通过相应平台的的Epoll实现,创建Selector对象,然后构造一个SelectedSelectionKeySet对象,这是Netty自己对SelectKeys的实现,然后通过反射将Selector对象中的selectedKeySet成员变量替换为自己的实现。此处应该全体起立喊666,哈哈!更绝的是Netty最新的4.x版本中加了一条:如果JDK版本大于等于9,连反射都不用了,直接通过Unsafe操作,通过成员变量的的偏移地址修改。有兴趣可以把代码拉下来参观参观。

2.3 关联EventLoop

前面说完了EventLoopGroup的实例化(包括EventLoop),也就是EventLoopGroup bossGroup = new NioEventLoopGroup(n)这行代码。

下面主要分析channel关联EventLoop。

channel关联Eventloop有三种情况:客户端SocketChannel关联EventLoop、服务端ServerSocketChannel关联EventLoop、由服务端ServerSocketChannel创建的SocketChannel关联EventLoop。Netty厉害的就是把这三种情况都都能复用Multithread EventLoopGroup中的register方法:

@Override
public ChannelFuture register(Channel channel) {
    return next().register(channel);
}

根据选择策略找到可用的EventLoop,然后调用SingleThreadEventLoop中的register方法,最终调用了 AbstractChannel#AbstractUnsafe.register 后


public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    // 删除条件检查.
    AbstractChannel.this.eventLoop = eventLoop;
    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        try {
            eventLoop.execute(new OneTimeTask() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            ...
        }
    }
}

将一个 EventLoop 赋值给 AbstractChannel 内部的 eventLoop 字段, 到这里就完成了 EventLoop 与 Channel 的关联过程。
但是上面代码一直是bind过程,也及时说在main线程中执行,所以会跳入else分支。将注册操作包装为一个Runnable,提交给eventloop的execute方法,该方法实现是在SingleThreadEventExecutor中实现的。

总结一下NioEventLoop,NioEventLoop是对IO 操作和线程的整合,那么什么时候启动NioEventLoop的线程?答案是注册Channel(向selector注册)的时候。这是Server端的第一步,因此在这里启动线程比较合适。
Netty

下面分析NoioEventLoop的任务处理机制。

3. NioEventLoop的任务处理机制

一个 NioEventLoop 通常需要肩负起两种任务, 第一个是作为 IO 任务, 处理 IO 操作,如accept、connect、read、write等。第二个就是非IO任务, 处理 taskQueue 中的任务,,如register0、bind0等任务。先回顾一下NIO中Selector的使用流程:

Java NIO流程
  1. 通过 Selector.open() 打开一个 Selector.

  2. 将 Channel 注册到 Selector 中, 并设置需要监听的事件(interest set)

  3. 不断重复:

    3.1 调用 select() 方法

    3.2 调用 selector.selectedKeys() 获取 selected keys

    3.3迭代每个 selected key:

    3.4 从 selected key 中获取 对应的 Channel 和附加信息(如果有的话)

    3.5 判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT 事件, 则调用 “SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()” 获取 SocketChannel, 并将它设置为 非阻塞的, 然后将这个 Channel 注册到 Selector 中.

    3.6 根据需要更改 selected key 的监听事件.

    3.7将已经处理过的 key 从 selected keys 集合中删除

3.1 register任务

第一步打开Selector,在实例化NioEventLoop时已经初始化完成,也保存至NioEventLoop的属性selector中。将channel注册到selector中通过上面2.3中的register0(promise)来完成,回顾一下注册的调用链:

Bootstrap.initAndRegister -> 
    AbstractBootstrap.initAndRegister -> 
        MultithreadEventLoopGroup.register -> 
            SingleThreadEventLoop.register -> 
                AbstractUnsafe.register -> //交给EventLoop执行
                	AbstractUnsafe.register0 -> 
                        AbstractNioChannel.doRegister

register0 又调用了 AbstractNioChannel.doRegister:

  @Override
protected void doRegister() throws Exception {
    selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
    return;
}

在这里 javaChannel() 返回的是一个 Java NIO SocketChannel 对象, 我们将此 SocketChannel 注册到 Selector 中。第二个是感兴趣的事件,0表示对所有事件都不感兴趣。这里只是将Channel注册到Selector。设置感兴趣的事件在ChannelActive事件中实现,具体的说是在HeadContext中实现的。具体逻辑请看https://blog.csdn.net/TheLudlows/article/details/82712942 。第三个参数this,是attch对象。相当于把当前的NioServerScoketChannel(Netty对JDK原生ServerSocketChannel的包装)放进去。这个会在后面的IO处理中用到。

3.2 启动线程,添加任务

在3.1节中所讲的只是channel注册任务,netty会把此任务提交给任务队列,通过execute方法去启动线程。同时,selector的操作流程只完成了两步,带着一个问题:还有最后一步循环调用select方法在哪完成的呢?execute方法实现了Java并发包Executor的接口方法,用来执行任务。

@Override
public void execute(Runnable task) {
	//此处为false,因为还在main线程中
    boolean inEventLoop = inEventLoop();
    if (inEventLoop) {
        addTask(task);
    } else {
        startThread();
        addTask(task);
        if (isShutdown() && removeTask(task)) {
            reject();
        }
    }
    if (!addTaskWakesUp && wakesUpForTask(task)) {
        wakeup(inEventLoop);
    }
}

如果当前运行的线程不是所绑定的线程则调用startThread方法为本EventExecutor绑定支撑线程,然后尝试启动线程(只能启动一次),随后再将任务添加到队列中去。如果线程已经停止,并且删除任务失败,则执行拒绝策略,默认是抛出异常。

private void startThread() {
    if (state == ST_NOT_STARTED) {
        if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
            try {
                doStartThread();
            } catch (Throwable cause) {
                STATE_UPDATER.set(this, ST_NOT_STARTED);
                PlatformDependent.throwException(cause);
            }
        }
    }
}

private void doStartThread() {
	assert thread == null;
	executor.execute(new Runnable() {
    @Override
    public void run() {
        thread = Thread.currentThread();
        if (interrupted) {
            thread.interrupt();
        }
        boolean success = false;
        updateLastExecutionTime();
        SingleThreadEventExecutor.this.run();
        success = true;
		//省略部分代码
	}
}

如果EventExecutor的状态为ST_NOT_STARTED,那么先修改状态然后调用doStartThread方法为本EventExecutor绑定线程,断言指出此时thread必须为null,表明当前EventExecutor还未绑定任何线程。绑定任务交由线程池调度执行,线程池中执行该任务的线程被绑定到EventExecutor上。然后设置最后一次的执行时间。执行当前 NioEventLoop 的 run 方法,注意:这个方法是个死循环,是整个 EventLoop 的核心。

@Override
    protected void run() {
        for (;;) {
            try {
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case -2:
                        continue;
                    case -1:
                        select(wakenUp.getAndSet(false));
                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                    default:
                }
                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);
            }
            try {
                if (isShuttingDown()) {
                    closeAll();
                    if (confirmShutdown()) {
                        return;
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
        }
    }

NioEventLoop 事件循环的核心就是这里,也就是Selector 使用步骤的第三步的部分。selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())判断任务队列是否有任务,如果有,那么调用一次selectNow方法,并返回selectNow的结果,如果没有,返回SelectStrategy.SELECT,这个值为-1。这段代码的内部实现很直观,在此就不列出。taskQueue有任务的时候调非阻塞的selectNow方法,以保证taskQueue中的任务可以尽快执行。

selector 返回后, 当 ioRatio 变量为100的时候(默认50),处理 select 事件,处理完之后执行任务队列中的所有任务。 反之当不是 100 的时候,处理 selecotr 事件,之后给定一个时间内执行任务队列中的任务。

ioRatio 的作用就是限制执行任务队列的时间。如果 ioRatio 比例是100 的话,则这个比例无作用。公式则是建立在 IO 时间上的,公式为 ioTime * (100 - ioRatio) / ioRatio ; 也就是说,当 ioRatio 是 10 的时候,IO 任务执行了 100 纳秒,则非IO任务将会执行 900 纳秒,直到没有任务可执行。

总结一下run方法:

  • NioEventLoop.select方法,调用底层的epoll select 方法获取read的事件
  • processSelectedKeys 中处理就绪的事件。
  • runAllTasks 执行队列中的任务

下面深入这3个方法进行讲解

3.3 核心select函数

NioEventLoop.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 (hasTasks() && wakenUp.compareAndSet(false, true)) {
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }

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

                if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                    break;
                }
                if (Thread.interrupted()) {
                    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) {
                    selectCnt = 1;
                } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                    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);
            }
        }
    }
  1. 使用当前时间加上定时任务即将执行的剩余时间(如果没有定时任务,默认1秒)。得到 selectDeadLineNanos。减去当前时间并加上一个缓冲值 0.5秒,得到一个 selecotr 阻塞超时时间。
  2. 如果这个值小于等于0,则立即 selecotNow 返回。
  3. 如果大于0,如果任务队列中有任务,并且 CAS 唤醒 selector 能够成功。立即返回
  4. int selectedKeys = selector.select(timeoutMillis),开始真正的阻塞(默认一秒钟),返回准备好的Channel数
  5. select 方法一秒钟返回后,如果有事件,或者 selector 被唤醒了,或者 任务队列有任务,或者定时任务即将被执行,跳出循环。
  6. 如果线程被中断了,则跳出循环。
  7. 如果一切正常,开始判断这次 select 的阻塞时候是否大于等于给定的 timeoutMillis 时间,如果没有,且循环了超过 512 次(默认),则认为触发了 JDK 的 epoll 空轮询 Bug,调用 rebuildSelector 方法重新创建 selector,并 selectorNow 立即返回。
3.4 处理就绪事件

当select方法返回后,意味着有IO时间需要处理了,继续调用processSelectedKeys方法。

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

判断 selectedKeys 这个变量,这个变量是一个 Set 类型,但 Netty 内部使用了 SelectionKey 类型的数组,而不是 Set实现,当想selector注册channel时,JDK 的 NIO 会向这个 set 添加 SelectionKey。当 selector 方法有返回值的时候,JDK NIO会Update 该集合。在Selector源码分析中有讲到。通过上面的代码我们看到,如果不是 null(默认开启优化) ,使用优化过的 SelectionKeys,也就是数组,如果没有开启优化,则使用 JDK 默认的。

private void processSelectedKeysOptimized() {
        for (int i = 0; i < selectedKeys.size; ++i) {
            final SelectionKey k = selectedKeys.keys[i];
            selectedKeys.keys[i] = null;
            final Object a = k.attachment();
            if (a instanceof AbstractNioChannel) {
                processSelectedKey(k, (AbstractNioChannel) a);
            } else {
                @SuppressWarnings("unchecked")
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                processSelectedKey(k, task);
            }
            if (needsToSelectAgain) {
                selectedKeys.reset(i + 1);
                selectAgain();
                i = -1;
            }
        }
    }

其实就俩个步骤迭代 selectedKeys 获取就绪的 IO 事件, 然后为每个事件都调用 processSelectedKey 来处理它。这也正是JavaNIO流程的3.3~3.7步。
先是遍历keys,k.attachment()取出对应的NioAbstractChannel,然后将key和NioAbstractChannel传入processSelectedKey处理。

private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
      
        try {
            int readyOps = k.readyOps();
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);
                unsafe.finishConnect();
            }

            // Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
            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
                ch.unsafe().forceFlush();
            }

            // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
            // to a spin loop
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }

现时获取到Channel自己的Unsafe,通过Unsafe去操作Channel底层的IO事件。如果selection key是:SelectionKey.OP_CONNECT,那表明这是TCP连接已经建立。一般客户端channel.register(selector, SelectionKey.OP_CONNECT)注册的事件,当该事件就绪,我们需要把这个selection key从intrestOps中清除掉,否则下次select操作会直接返回。接下来调用finishConnect方法,表明连接完成。如果是写事件就绪,就把数据刷到客户端,实现代码很直观。关于读事件和accept事件就绪,也就是最后一个if语句的分析下篇文章分析。

4. 任务队列机制

关于第3.2节中提到的第三个方法runAlltask,将放在本小节中讲述,因为任务队列的内容较多,同样也非常重要。因此单独抽出来和任务的添加一开块讲述。

4.1 是否添加至队列?

继续回到addTask的起点,还是以register举例:

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    // 删除条件检查.
    ...
    AbstractChannel.this.eventLoop = eventLoop;
    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        try {
            eventLoop.execute(new OneTimeTask() {
              ...	
            });
        } catch (Throwable t) {
            ...
        }
    }
}

把ServerSocketChannel和EventLoop关联完了之后,需要判断当前线程是否是关联的EventLoop中的线程,为什么需要判断呢?前面分析的是启动过程的bind操作调用。一直在主线程中执行。你能猜到了,难道有不在主线程中执行的。没错,这个register方法在AbstractChannel中内部类AbstractUnsafe中实现的方法。是一个极度“抽象的方法”,不管是ServerSocketChannel还是SocketChannel向selector注册都会调用这个方法。

比如就注册这个任务,用线程池解决的话,一股脑的将任务提交进去会产生一些问题,任务队列的过大导致OOM,线程频繁切换带来的额外资源消耗等。假如任务的执行速度够快,但是短时间内任务执行导致线程切换
所带来的消耗也是不可忽视的。
ServerSocketChannel的注册一般会在主线程中执行,但是SocketChannel的注册一定不是在主线程中的。
摘自《Netty in action》中一段话:

Netty线程模型的卓越性能取决于对于当前执行线程的身份的确定,也就是说,确定它是否是分配给当前Channel以及它的EventLoop的那一个线程。如果(当前)调用线程正是支撑EventLoop的线程,那么所提交的代码块将会被(直接)执行。否则,EventLoop将调度该任务以便稍后执行,并将它放入到内部队列中。当EventLoop下次处理它的事件时,它会执行队列中的那些任务。这也就解释了任何的线程是如何与Channel直接交互而无需在ChannelHandler中进行额外同步的。

进入execute方法,内部实现在此检测线程。如果当前线程是EventLoop中的线程那么直接添加到任务队列,如果不是则启动线程。

 @Override
public void execute(Runnable task) {
	boolean inEventLoop = inEventLoop();
	if (inEventLoop) {
	    addTask(task);
	} else {
	    startThread();
	    addTask(task);
	    if (isShutdown() && removeTask(task)) {
	        reject();
	    }
	}
	//省略无用代码
}

另外addTask是将任务添加到SingleThreadEventExecutor的askQueue中,而schedule将会添加到AbstractScheduledEventExecutor的scheduledTaskQueue中,类似于计划任务线程池中的DelayedWorkQueue。

4.2 计划任务队列

实现任务队列的功能在超类 SingleThreadEventExecutor 实现的, 而 schedule 功能的实现是在SingleThreadEventExecutor 的父类, 即 AbstractScheduledEventExecutor 中实现的.
AbstractScheduledEventExecutor 所实现的 schedule 方法:

@Override
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
    return schedule(new ScheduledFutureTask<V>(
            this, callable, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay))));
}

这是其中一个重载的 schedule, 当一个 Runnable 传递进来后, 会被封装为一个 ScheduledFutureTask 对象, 这个对象会记录下这个 Runnable 在何时运行、已何种频率运行等信息。当构建了 ScheduledFutureTask 后, 会继续调用 另一个重载的 schedule 方法:

<V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {
    if (inEventLoop()) {
        scheduledTaskQueue().add(task);
    } else {
        execute(new Runnable() {
            @Override
            public void run() {
                scheduledTaskQueue().add(task);
            }
        });
    }
    return task;
}

ScheduledFutureTask 对象就会被添加到 scheduledTaskQueue 中

4.2 任务的执行

执行队列中的任务其实就是NioEventLoop类中的runAllTask,runAllTasks 方法有两个重载的方法, 一个是无参数的, 另一个有一个参数的。带参数就是限制任务执行的时间。

 protected boolean runAllTasks() {
    assert inEventLoop();
    boolean fetchedAll;
    boolean ranAtLeastOne = false;
    do {
        fetchedAll = fetchFromScheduledTaskQueue();
        if (runAllTasksFrom(taskQueue)) {
            ranAtLeastOne = true;
        }
    } while (!fetchedAll); // keep on processing until we fetched all scheduled tasks.

    if (ranAtLeastOne) {
        lastExecutionTime = ScheduledFutureTask.nanoTime();
    }
    afterRunningAllTasks();
    return ranAtLeastOne;
}

将定时任务队列PriorityQueue 类型的 scheduledTaskQueue中即将执行的任务都添加到普通的任务队列taskQueu中。

private boolean fetchFromScheduledTaskQueue() {
    long nanoTime = AbstractScheduledEventExecutor.nanoTime();
    Runnable scheduledTask  = pollScheduledTask(nanoTime);
    while (scheduledTask != null) {
    	// 如果添加到taskQueue中失败,再次放回至scheduledTaskQueue
        if (!taskQueue.offer(scheduledTask)) {
            scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);
            return false;
        }
        scheduledTask  = pollScheduledTask(nanoTime);
    }
    return true;// 走到这里说明可执行的计划任务全部添加完毕
}

接着执行taskQueue中的任务,也就是runAllTasksFrom方法。

protected final boolean runAllTasksFrom(Queue<Runnable> taskQueue) {
    Runnable task = pollTaskFrom(taskQueue);
    if (task == null) {
        return false;
    }
    for (;;) {
        safeExecute(task);
        task = pollTaskFrom(taskQueue);
        if (task == null) {
            return true;
        }
    }
}

直至全部执行完毕。接着执行afterRunningAllTasks,也就是执行tailTask中的任务,一般不会用到tailTask此处就不在介绍。

至此NioEventLoop的分析结束。断断续续写了三周,遗留两个问题,一是accept事件的处理,二是read事件的处理。将会在后面的文章中分析。

最后以一句话结束:最完美的IO模型和线程模型的结合唯有Netty,没有之一。

5. EpollEventLoop

EpollEventLoop以及EpollEventLoopGroup是netty为Linux打造的API,基于JNI实现,有如下优势

  1. 使用 epoll edge-triggered 而 Java的 nio 使用 level-triggered
  2. 暴露了更多的nio没有的配置参数, 如 TCP_CORK, SO_REUSEADDR等。
  3. C代码,更少GC

最具亮点的特性当数SO_REUSEPORT,可以将多个线程绑定到一个端口。用法上NioEventLoop几乎一样。但是只有linux支持。具体SO_REUSEPORT实现原理见如下链接。

https://netty.io/wiki/native-transports.html
http://lists.dragonflybsd.org/pipermail/users/2013-July/053632.html
http://www.blogjava.net/yongboy/archive/2015/02/12/422893.html

  • 14
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值