EventLoop与EventLoopGroup

转载自:https://blog.csdn.net/u010853261/article/details/62043709
    https://blog.csdn.net/u010412719/article/details/78107741

Reactor线程模型

  Reactor线程模型有三种

  • 单线程模型
  • 多线程模型
  • 主从Reactor线程模型

  关于这三种线程模型的原型,可以参看:https://blog.csdn.net/u010853261/article/details/55805216

NioEventLoopGroup与Reactor线程模型的对应

  前面介绍了Reactor线程模型的原型实现,那么NIOEventLoopGroup是怎么与Reactor关联在一起的呢?其实NIOEventLoopGroup就是一个线程池实现,通过设置不同的NIOEventLoopGroup方式就可以对应三种不同的Reactor线程模型。
这里我只给出服务端的配置,对于客户端都是一样的。
  
  单线程模型

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup)
     .channel(NioServerSocketChannel.class);
//.....

  上面实例化了一个NIOEventLoopGroup,构造参数是1表示是单线程的线程池。然后接着调用 b.group(bossGroup) 设置了服务器端的 EventLoopGroup. 当传入一个 group 时, 那么 bossGroup 和 workerGroup 就是同一个 NioEventLoopGroup 了.
  这时候呢, 因为 bossGroup 和 workerGroup 就是同一个 NioEventLoopGroup, 并且这个 NioEventLoopGroup 只有一个线程, 这样就会导致 Netty 中的 acceptor 和后续的所有客户端连接的 IO 操作都是在一个线程中处理的. 那么对应到 Reactor 的线程模型中, 我们这样设置 NioEventLoopGroup 时, 就相当于 Reactor 单线程模型.

  多线程模型

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

  bossGroup 中只有一个线程, 在workerGroup线程池中没有指定线程数量,所以默认是 CPU 核心数乘2, 因此对应的到 Reactor 线程模型中, 这样设置的 NioEventLoopGroup 其实就是 Reactor 多线程模型.

  主从Reactor线程模型

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

  Netty 的服务器端的 acceptor 阶段, 没有使用到多线程, 因此上面的主从多线程模型 在 Netty 的服务器端是不存在的.
  服务器端的 ServerSocketChannel 只绑定到了 bossGroup 中的一个线程, 因此在调用 Java NIO 的 Selector.select 处理客户端的连接请求时, 实际上是在一个线程中的, 所以对只有一个服务的应用来说, bossGroup 设置多个线程是没有什么作用的, 反而还会造成资源浪费。

Netty的线程模型

  Netty的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。Netty的多线程编程最佳实践如下:
  1)创建两个NioEventLoopGroup,用于逻辑隔离NIO Acceptor和NIO I/O线程。
  2)尽量不要在ChannelHandler中启动用户线程(解码后用于将POJO消息派发到后端业务线程的除外)。
  3)解码要放在NIO线程调用的解码Handler中进行,不要切换到用户线程中完成消息的解码。
  4)如果业务逻辑操作非常简单,没有复杂的业务逻辑计算,没有可能会导致线程被阻塞的磁盘操作、数据库操作、网路操作等,可以直接在NIO线程上完成业务逻辑编排,不需要切换到用户线程。
  5)如果业务逻辑处理复杂,不要在NIO线程上完成,建议将解码后的POJO消息封装成Task,派发到业务线程池中由业务线程执行,以保证NIO线程尽快被释放,处理其他的I/O操作。

NioEventLoopGroup

  Netty内部都是通过线程在处理各种数据,EventLoopGroup就是用来管理调度他们的,注册Channel,管理他们的生命周期,下面就来看看EventLoopGroup是怎样工作的(基于4.1.11.Final源码)。

public class NioEventLoopGroup extends MultithreadEventLoopGroup {

    /**
     * Create a new instance using the default number of threads, the default {@link ThreadFactory} and
     * the {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}.
     */
    public NioEventLoopGroup() {
        this(0);
    }

    /**
     * Create a new instance using the specified number of threads, {@link ThreadFactory} and the
     * {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}.
     */
    public NioEventLoopGroup(int nThreads) {
        this(nThreads, (Executor) null);
    }

    /**
     * Create a new instance using the specified number of threads, the given {@link ThreadFactory} and the
     * {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}.
     */
    public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory) {
        this(nThreads, threadFactory, SelectorProvider.provider());
    }

    public NioEventLoopGroup(int nThreads, Executor executor) {
        this(nThreads, executor, SelectorProvider.provider());
    }

    //...略

    public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory,
        final SelectorProvider selectorProvider, final SelectStrategyFactory selectStrategyFactory) {
        super(nThreads, threadFactory, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
    }

    //...略

  NioEventLoopGroup会调用其父类MultithreadEventLoopGroup的构造函数:

    /**
     * @see MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, Object...)
     */
    protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
        super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
    }

  这里,内部线程数DEFAULT_EVENT_LOOP_THREADS的大小是:
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

  MultithreadEventLoopGroup的父类是MultithreadEventExecutorGroup:

    protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
        if (nThreads <= 0) {
            throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
        }

        if (executor == null) {
            executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
        }

        children = new EventExecutor[nThreads];

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

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

  children 是EventExecutor数组对象,其大小是 nThreads, 这样就构成了一个线程池,里面存放的是通过NioEventLoopGroup的newChild方法生成的NioEventLoop对象:

    @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]);
    }

  注意到这里,children中所有的线程公用executorSelectorProviderSelectStrategyFactory以及RejectedExecutionHandler。SelectorProvider是通过SelectorProvider.provider()获取的,而这个方法返回的是一个单例SelectorProvider。至此,Group和内部的Loop对象以及Executor就创建完毕。

NioEventLoop

  NioEventLoop继承于SingleThreadEventLoop, 而SingleThreadEventLoop又继承于SingleThreadEventExecutor,SingleThreadEventExecutor继承于AbstractScheduledEventExecutor。
  SingleThreadEventExecutor是Netty中对本地线程的抽象, 它内部有一个Thread thread属性, 存储了一个本地Java线程。因此可以认为, 一个NioEventLoop其实和一个特定的线程绑定, 并且在其生命周期内, 绑定的线程都不会再改变。
  在AbstractScheduledEventExecutor中,Netty 实现了NioEventLoop的schedule功能, 即可以通过调用一个NioEventLoop 实例的schedule方法来运行一些定时任务. 而在SingleThreadEventLoop中, 又实现了任务队列的功能, 通过它, 可以调用一个NioEventLoop实例的execute方法来向任务队列中添加一个 task,并由 NioEventLoop 进行调度执行。
  通常来说, NioEventLoop 肩负着两种任务, 第一个是作为IO线程, 执行与 Channel 相关的IO操作, 包括调用 select 等待就绪的IO事件、读写数据与数据的处理等; 而第二个任务是作为任务队列, 执行taskQueue中的任务, 例如用户调用eventLoop.schedule提交的定时任务也是这个线程执行的。
  下面是它的具体的继承关系图:

  它实现了EventLoop接口、EventExecutorGroup接口和ScheduledExecutorService接口,正是因为这种设计,导致NioEventLoop和其父类功能实现非常复杂。
  作为NIO框架的Reactor线程,NioEventLoop需要处理网络I/O读写事件,因此它必须聚合一个Selector对象。在NioEventLoop构造时将创建并打开一个新的Selector。Netty对Selector的selectedKeys进行了优化,用户可以通过io.netty.noKeySetOptimization开关决定是否启用该优化项。默认不打开selectedKeys优化功能。



  对于NioEventLoop的实例化,基本就是在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]);
}
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
             SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
    super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
    if (selectorProvider == null) {
        throw new NullPointerException("selectorProvider");
    }
    if (strategy == null) {
        throw new NullPointerException("selectStrategy");
    }
    provider = selectorProvider;
    final SelectorTuple selectorTuple = openSelector();
    selector = selectorTuple.selector;
    unwrappedSelector = selectorTuple.unwrappedSelector;
    selectStrategy = strategy;
}

  来看NioEventLoop的run:

protected void run() {
    /** 死循环:NioEventLoop 事件循环的核心就是这里! */
    for (;;) {
        try {
            // 通过 select/selectNow 调用查询当前是否有就绪的 IO 事件
            // 当 selectStrategy.calculateStrategy() 返回的是 CONTINUE, 就结束此轮循环,进入下一轮循环;
            // 当返回的是 SELECT, 就表示任务队列为空,就调用select(Boolean);
            switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                case SelectStrategy.CONTINUE:
                    continue;
                case SelectStrategy.SELECT:
                    select(wakenUp.getAndSet(false));

                    if (wakenUp.get()) {
                        selector.wakeup();
                    }
                default:
                    // fallthrough
            }// end switch

            // 当有IO事件就绪时, 就会处理这些IO事件
            cancelledKeys = 0;
            needsToSelectAgain = false;

            // ioRatio表示:此线程分配给IO操作所占的时间比(即运行processSelectedKeys耗时在整个循环中所占用的时间).
            final int ioRatio = this.ioRatio;
            if (ioRatio == 100) {
                try {
                    //查询就绪的 IO 事件, 然后处理它;
                    processSelectedKeys();
                } finally {
                    //运行 taskQueue 中的任务.
                    runAllTasks();
                }
            } else {
                final long ioStartTime = System.nanoTime();
                try {
                    //查询就绪的 IO 事件, 然后处理它;
                    processSelectedKeys();
                } finally {
                    // Ensure we always run tasks.
                    final long ioTime = System.nanoTime() - ioStartTime;
                    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                }
            }
        } catch (Throwable t) {
            handleLoopException(t);
        }
        //...略
    }
}

  上面函数中的一个死循环 for(;;) 就是NioEventLoop事件循环执行机制。下面对上面过程进行详解,分两步:IO事件轮询、IO事件的处理。
  1.IO事件轮询
  首先, 在run()方法中, 第一步是调用hasTasks()方法来判断当前任务队列中是否有任务:

    protected boolean hasTasks() {
        assert inEventLoop();
        return !taskQueue.isEmpty();
    }    

  这个方法很简单, 仅仅是检查了一下 taskQueue 是否为空。至于 taskQueue 是什么呢, 其实它就是存放一系列的需要由此 EventLoop 所执行的任务列表。
  1)当 taskQueue 不为空时, hasTasks() 就会返回TRUE,那么selectStrategy.calculateStrategy() 的实现里面就会执行selectSupplier.get()get()方法里面会调用 selectNow();:

     void selectNow() throws IOException {
        try {
            selector.selectNow();
        } finally {
            // restore wakup state if needed
            if (wakenUp.get()) {
                selector.wakeup();
            }
        }
    }

  这个 selector 字段正是 Java NIO 中的多路复用器 Selector(比如KQueueSelectorImpl实例)。selector.selectNow()立即返回当前就绪的IO事件的个数,如果存在IO事件,那么在switch 语句中就会直接执行 default,直接跳出switch语句,如果不存在,就是返回0,对应于continue,忽略此次循环。
  2)当taskQueue为空时,selectStrategy.calculateStrategy() 就会返回SelectStrategy.SELECT,对于switch case语句就是执行select()函数,阻塞等待IO事件就绪。
  selector.select()是选择一些I/O操作已经准备好的管道。每个管道对应着一个key。这个方法是一个阻塞的选择操作。当至少有一个通道被选择时才返回。当这个方法被执行时,当前线程是允许被中断的。selector.selectNow()selector.select()的区别在于,是非阻塞的,即当前操作即使没有通道准备好也是立即返回,只是返回的是0,不会阻塞当前线程.

  2.IO事件处理
  在 NioEventLoop.run() 方法中, 第一步是通过 select/selectNow 调用查询当前是否有就绪的 IO 事件. 那么当有 IO 事件就绪时, 第二步自然就是处理这些 IO 事件啦.首先让我们来看一下 NioEventLoop.run 中循环的剩余部分(核心部分):

final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
    processSelectedKeys();
    runAllTasks();
} else {
    final long ioStartTime = System.nanoTime();

    processSelectedKeys();

    final long ioTime = System.nanoTime() - ioStartTime;
    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}

  上面列出的代码中, 有两个关键的调用, 第一个是 processSelectedKeys() 调用, 根据字面意思, 我们可以猜出这个方法肯定是查询就绪的 IO 事件, 然后处理它; 第二个调用是 runAllTasks(), 这个方法我们也可以一眼就看出来它的功能就是运行 taskQueue 中的任务.
  ioRatio表示的是此线程分配给 IO 操作所占的时间比(即运行 processSelectedKeys 耗时在整个循环中所占用的时间). 例如 ioRatio 默认是 50, 则表示 IO 操作和执行 task 的所占用的线程执行时间比是 1 : 1. 当知道了 IO 操作耗时和它所占用的时间比, 那么执行 task 的时间就可以很方便的计算出来了。
  当ioRatio为100时, Netty 就不考虑 IO 耗时的占比, 而是分别调用 processSelectedKeys()、runAllTasks(); 而当 ioRatio 不为 100时, 则执行到 else 分支, 在这个分支中, 首先记录下 processSelectedKeys() 所执行的时间(即 IO 操作的耗时), 然后根据公式, 计算出执行 task 所占用的时间, 然后以此为参数, 调用 runAllTasks().

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值