Netty4.1源码阅读——核心(EventLoop)

469 阅读10分钟

前言

还记得使用netty的时候会出现这么一段代码吗:

	EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

EventLoopGroup在没看源码之前,我把他理解为是一个管理EventLoop的组,而EventLoop是netty封装好的线程池模型,不知道我这样想得对不对,分析一下源码。

正文

EventLoopGroup

EventLoopGroup的接口图

image.png

public interface EventExecutorGroup extends ScheduledExecutorService, Iterable<EventExecutor>

根据这个接口图和接口代码来看,EventExecutorGroup应当具有这两个属性:支持定时任务线程池、内部保存了多个EventExecutor

public interface EventLoopGroup extends EventExecutorGroup {
    /**
     * 获取下一个EventLoop
     */
    @Override
    EventLoop next();

    ChannelFuture register(Channel channel);

    ChannelFuture register(ChannelPromise promise);

    @Deprecated
    ChannelFuture register(Channel channel, ChannelPromise promise);
}

EventLoopGroup仅有几个注册方法和next方法,EventLoopGroup会调用next去选择一个EventLoop来调用,所以先看一下EventLoop以及子类的实现。


EventLoop

public interface EventLoop extends OrderedEventExecutor, EventLoopGroup {
    @Override
    EventLoopGroup parent();
}

EventLoop接口只有一个方法,就是和EventLoopGroup关联

public abstract class AbstractEventLoop extends AbstractEventExecutor implements EventLoop {

    protected AbstractEventLoop() { }

    protected AbstractEventLoop(EventLoopGroup parent) {
        super(parent);
    }

    @Override
    public EventLoopGroup parent() {
        return (EventLoopGroup) super.parent();
    }

    @Override
    public EventLoop next() {
        return (EventLoop) super.next();
    }
}

它的一个子类,继承的AbstractEventExecutor,进入这个类看看

public abstract class AbstractEventExecutor extends AbstractExecutorService implements EventExecutor

AbstractExecutorService内部实现了线程池的submit等方法,是JAVA内部线程池的一个抽象实现,这里继承这个类也是想把EventLoop变成一个类似线程池的东西

public interface EventExecutor extends EventExecutorGroup {
    
    @Override
    EventExecutor next();
    
    EventExecutorGroup parent();
    
    boolean inEventLoop();
    
    boolean inEventLoop(Thread thread);
    
    <V> Promise<V> newPromise();
    
    <V> ProgressivePromise<V> newProgressivePromise();
    
    <V> Future<V> newSucceededFuture(V result);
    
    <V> Future<V> newFailedFuture(Throwable cause);
}

EventExecutor比较主要的方法是inEventLoop(),表示当前线程是否为启动线程。

AbstractEventExecutor并没有太多实现细节,因为线程的提交方法已经交给了JAVA内部去实现,所以继续向下看

public abstract class AbstractScheduledEventExecutor extends AbstractEventExecutor {
    private static final Comparator<ScheduledFutureTask<?>> SCHEDULED_FUTURE_TASK_COMPARATOR =
            new Comparator<ScheduledFutureTask<?>>() {
                @Override
                public int compare(ScheduledFutureTask<?> o1, ScheduledFutureTask<?> o2) {
                    return o1.compareTo(o2);
                }
            };

   static final Runnable WAKEUP_TASK = new Runnable() {
       @Override
       public void run() { }
    };

    /**
     * 优先队列 通过比较时间 时间相同比较ID
     */
    PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue;

AbstractScheduledEventExecutor是定时任务接口的实现,可以看到这里定义了一个比较器,用于对ScheduledFutureTask的比较

@Override
    public int compareTo(Delayed o) {
        if (this == o) {
            return 0;
        }

        ScheduledFutureTask<?> that = (ScheduledFutureTask<?>) o;
        long d = deadlineNanos() - that.deadlineNanos();
        if (d < 0) {
            return -1;
        } else if (d > 0) {
            return 1;
        } else if (id < that.id) {
            return -1;
        } else {
            assert id != that.id;
            return 1;
        }
    }

定时任务很好理解,先根据时间比较,时间相同根据id比较

 final ScheduledFutureTask<?> peekScheduledTask() {
        Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue;
        return scheduledTaskQueue != null ? scheduledTaskQueue.peek() : null;
    }

获取一个queue顶的任务(最早的任务),使用了队列的peek方法

 protected final Runnable pollScheduledTask(long nanoTime) {
        assert inEventLoop();

        ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
        if (scheduledTask == null || scheduledTask.deadlineNanos() - nanoTime > 0) {
            return null;
        }
        scheduledTaskQueue.remove();
        scheduledTask.setConsumed();
        return scheduledTask;
    }

pollScheduledTask的参数是时间,从queue拿出一个任务,通过对时间的比较来确定是否从队列中取出

以上是延迟任务的关键方法,定义了一个队列,所有的一切都在队列上进行操作;继续向下看:SingleThreadEventExecutor

image.png

image.png

SingleThreadEventExecutor内部才像一个线程池应该有的东西,比如生命周期等。

protected SingleThreadEventExecutor(
            EventExecutorGroup parent, ThreadFactory threadFactory, boolean addTaskWakesUp) {
        this(parent, new ThreadPerTaskExecutor(threadFactory), addTaskWakesUp);
    }

看一个普通的构造方法,传入了threadFactory用于创建线程,在netty内部创建的线程都是FastThreadLocalThread,用于支持FastThreadLocal

 /**
     * poll一个queue 不能是WAKEUP_TASK
     */
    protected Runnable pollTask() {
        assert inEventLoop();
        return pollTaskFrom(taskQueue);
    }

    protected static Runnable pollTaskFrom(Queue<Runnable> taskQueue) {
        for (;;) {
            Runnable task = taskQueue.poll();
            if (task != WAKEUP_TASK) {
                return task;
            }
        }
    }

获取一个task的方法,调用了队列的poll方法

关注一下takeTask方法

protected Runnable takeTask() {
        assert inEventLoop();
        if (!(taskQueue instanceof BlockingQueue)) {
            throw new UnsupportedOperationException();
        }

        BlockingQueue<Runnable> taskQueue = (BlockingQueue<Runnable>) this.taskQueue;
        for (;;) {
            ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
            if (scheduledTask == null) {
                Runnable task = null;
                try {
                    task = taskQueue.take();
                    if (task == WAKEUP_TASK) {
                        task = null;
                    }
                } catch (InterruptedException e) {
                    // Ignore
                }
                return task;
            } else {
                long delayNanos = scheduledTask.delayNanos();
                Runnable task = null;
                if (delayNanos > 0) {
                    try {
                        task = taskQueue.poll(delayNanos, TimeUnit.NANOSECONDS);
                    } catch (InterruptedException e) {
                        // Waken up.
                        return null;
                    }
                }
                if (task == null) {
                    fetchFromScheduledTaskQueue();
                    task = taskQueue.poll();
                }

                if (task != null) {
                    return task;
                }
            }
        }
    }

private boolean fetchFromScheduledTaskQueue() {
        if (scheduledTaskQueue == null || scheduledTaskQueue.isEmpty()) {
            return true;
        }
        long nanoTime = AbstractScheduledEventExecutor.nanoTime();
        for (;;) {
            Runnable scheduledTask = pollScheduledTask(nanoTime);
            if (scheduledTask == null) {
                return true;
            }
		//普通任务队列没有空间则重新放回
            if (!taskQueue.offer(scheduledTask)) {
                scheduledTaskQueue.add((ScheduledFutureTask<?>) scheduledTask);
                return false;
            }
        }
    }

首先这个方法是阻塞方法,如果获取不到就会一直循环,具体步骤:

  1. 从定时任务获取队列获取最快的那个任务
  2. 如果没有定时任务,则获取普通任务;如果有定时任务,则按照定时任务的时间作为超时时间去普通队列获取。
  3. 如果在指定时间内没有获取到普通任务,需要定时任务拿出来放入普通任务队列,然后再获取(如果不进行次操作的话,当普通任务队列有任务,将永远执行不了定时任务)
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;
            }
        }
    }

protected static void safeExecute(Runnable task) {
        try {
            task.run();
        } catch (Throwable t) {
            logger.warn("A task raised an exception. Task: {}", task, t);
        }
    }

执行所有任务,循环将任务取出来,然后运行。这里可以看出来,SingleThreadEventExecutor并没有像传统线程池一样,将任务取出来交给一个新的线程执行,而是用了串行的方式来执行任务

image.png

只看一波部分doStartThread方法,可以看出来核心调用run方法,而run是个抽象方法交给子类实现,直接快进到NioEventLoop

image.png

NioEventLoop里面有令人熟悉JAVA底层的东西了,Selector、SelectorProvider都是java nio的实现

image.png

构造方法里面执行了一个openSelector的方法,这个方法还比较复杂分为两部分看

NioEventLoop有一个配置是DISABLE_KEY_SET_OPTIMIZATION,表示是否开启键优化

image.png

因为在Selector内部,key是用Set来存储的,键优化就是针对这个而进行修改

image.png

调用了openSelector(),获取了一个选择器,当没有开启优化的时候直接返回,如果开启了键优化,就要执行如下操作:

  1. 手动加载sun.nio.ch.SelectorImpl这个类,并且创建一个SelectedSelectionKeySet,这个里面用数组的方式存储SelectionKey而不用Set,减少了hash碰撞带来的开销。
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;
                }
            }
        });

final Class<?> selectorImplClass = (Class<?>) maybeSelectorImplClass;
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
  1. 利用unsafe获取selectedKeys和publicSelectedKeys的偏移量,并且将数据put到SelectedSelectionKeySet这个类的数组中
 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.
                    }
  1. 将selectedKeys和publicSelectedKeys权限改为可访问,把原有的set替换为数组
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);

看一下重写的run方法

image.png

nextScheduledTaskDeadlineNanos获取定时队列第一个任务的超时时间,然后检查普通队列是否有任务,没有就按照这个超时时间用select去阻塞获取。这里有个问题,如果定时任务超时时间还很长,这个select就会一直阻塞下去吗?当然不会,只要有新的任务提交,就会触发wakeup

protected void wakeup(boolean inEventLoop) {
        if (!inEventLoop && nextWakeupNanos.getAndSet(AWAKE) != AWAKE) {
            selector.wakeup();
        }
    }

只要通过execute提交一个task,就会触发wakeup让select不再阻塞

image.png

当select返回大于0,表示已经有事件准备好了,这里的selectCnt是用于解决Selector的空轮询BUG,若Selector的轮询结果为空,也没有wakeup或新消息处理,则发生空轮询,CPU使用率100%,如果这个selectCnt达到了最大值,则需要重新构造Selector

ioRatio是用于控制I/O操作的比率,像read、write这些操作就是I/O操作,而对于taskQueue的获取添加运行,任务的运行就叫非I/O操作

当I/O操作为100时候,就要先处理准备好的SelectedKeys

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

这里的逻辑是对应了开启键优化和未开启,其实都差不多,一个是利用iterator来遍历,一个是利用数组遍历。

image.png

针对attachment的不同有不同的处理逻辑,看一下processSelectedKey

image.png

根据readyOps不同做不同的操作,最终还是交给了Channel的unsafe进行处理


image.png

通过这个接口的继承,可以看到EventLoop具有线程池属性,而NioEventLoop内部使用了无锁化的操作,将任务串行化避免多线程锁竞争带来的性能影响,从表面上看串行化不能更好的应对并发问题,但是可以启动多个串行化的线程一起工作,这种局部串行化整体多线程的设计方式,相比于一个任务队列多个线程的模式更优

EventExecutorGroup

EventExecutorGroup是用于管理多个EventExecutor的类

EventExecutor next();

其中next方法用于选择一个EventExecutor,先看AbstractEventExecutorGroup的第一个抽象实现类

image.png

这个类没什么特点,内部全部方法都是调用next选择出一个EventExecutor,使用之前解析过的方法。那其实核心方法就是next方法的实现,继续向下看

image.png

MultithreadEventExecutorGroup内部有一个EventExecutor的数组以及一个Set,看构造方法

image.png

代码简单,步骤如下:

  1. 实例化Executor,默认为newDefaultThreadFactory,newThread源码如下:
@Override
    public Thread newThread(Runnable r) {
        Thread t = newThread(FastThreadLocalRunnable.wrap(r), prefix + nextId.incrementAndGet());
        try {
            if (t.isDaemon() != daemon) {
                t.setDaemon(daemon);
            }

            if (t.getPriority() != priority) {
                t.setPriority(priority);
            }
        } catch (Exception ignored) {
            // Doesn't matter even if failed to set.
        }
        return t;
    }

    protected Thread newThread(Runnable r, String name) {
        return new FastThreadLocalThread(threadGroup, r, name);
    }

可以看到使用的是FastThreadLocalThread

  1. 根据线程数量创建EventExecutor数组
  2. 调用newChild方法来实例化EventExecutor,newChild是一个抽象方法,由子类实现
  3. 出错后的关闭操作

再看一下next方法

@Override
    public EventExecutor next() {
        return chooser.next();
    }

这里调用了EventExecutorChooser的next方法,这个类有两个不同的实现方法:

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

检查线程数是不是2的n次方

  • PowerOfTwoEventExecutorChooser
private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
        private final AtomicInteger idx = new AtomicInteger();
        private final EventExecutor[] executors;

        PowerOfTwoEventExecutorChooser(EventExecutor[] executors) {
            this.executors = executors;
        }

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

id自增然后通过&来进行选取,当executors的长度为2的n次方时,使用&的方式比%效率要高一些,这也是netty的优化

  • GenericEventExecutorChooser
private static final class GenericEventExecutorChooser implements EventExecutorChooser {
        // will encounter this in practice.
        private final AtomicLong idx = new AtomicLong();
        private final EventExecutor[] executors;

        GenericEventExecutorChooser(EventExecutor[] executors) {
            this.executors = executors;
        }

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

这就是普通的取余的方法

MultithreadEventExecutorGroup主要是定义了构造方法和next选择的方法,next主要思想就是按照数组顺序获取

他的下一个子类没有什么干什么事情,只是将CPU的默认核心数量的两倍当成默认线程数

image.png

继续向下看NioEventLoopGroup这个实现类,对应了NioEventLoop,这个类只是重写了构造方法以及关键的newChild实现

@Override
    protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null;
        return new NioEventLoop(this, executor, (SelectorProvider) args[0],
            ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2], queueFactory);
    }

以及对EventLoop的组操作

//设置I/O比率
public void setIoRatio(int ioRatio) {
        for (EventExecutor e: this) {
            ((NioEventLoop) e).setIoRatio(ioRatio);
        }
    }
//重构Selector
public void rebuildSelectors() {
        for (EventExecutor e: this) {
            ((NioEventLoop) e).rebuildSelector();
        }
    }

总结

EventLoop支持提交任务和定时任务,并且直接与JAVA NIO进行交互。而EventLoopGroup则是绑定多个EventLoop,通过线性循环选择的方式拿出一个EventLoop进行操作。我们使用netty的时候会创建两个EventLoopGroup,bossEventLoop用于处理连接,work用于处理io请求,里面的每一个EventLoop都会打开一个Selector去监听事件底层事件的发生,当有事件发生的时候调用unsafe方法