2. Netty解析:NioEventLoopGroup事件循环组

什么是事件循环组

   我们在Echo Server这个Netty的小Demo的启动代码中看到,无论是server端还是client端,一上来都实现创建单个事件循环组(客户端)或者父子事件循环组(服务端往往是创建两个,而且父循环组传入的参数是1,如果只绑定一个端口,那么填入1就好了),然后调用b.group()方法关联循环组。
   那啥是事件循环组呢,从名字来推测,它的基本元素应该是EventLoop事件循环。可以说,NioEventLoopNioEventLoopGroup是netty卓越的Reactor模型的重要体现。一个事件循环对应了一个Selector以及一个线程,用于接收处理IO事件。而事件循环组就是事件循环的管理者。在Server端,事件循环组往往是两个,一个主要用于连接的接收,而另外一个负责读写事件。

   我们看一看NioEventLoopGroup的源码。跟踪NioEventLoopGroup的构造方法,会调用到下面的构造器,意思是当我们没有指定NioEventLoopGroup的构造器参数的时候,nThread=0,那么传入的线程数就变为cpu核心数乘以2.

    protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
        super(nThreads == 0? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args);
    }
    protected MultithreadEventExecutorGroup(int nThreads, ThreadFactory threadFactory, Object... args) {
        if (nThreads <= 0) {
            throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
        }

        if (threadFactory == null) {
            threadFactory = newDefaultThreadFactory();
        }

        children = new SingleThreadEventExecutor[nThreads];
        if (isPowerOfTwo(children.length)) {
            chooser = new PowerOfTwoEventExecutorChooser();
        } else {
            chooser = new GenericEventExecutorChooser();
        }

        for (int i = 0; i < nThreads; i ++) {
            boolean success = false;
            try {
                children[i] = newChild(threadFactory, 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 {
                // 创建失败时的处理            
            }
        }

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

   先调试看一下当程序执行到MultithreadEventExecutorGroup构造器时,传入的参数是啥。

MultithreadEventExecutorGroup构造器参数

然后看一下NioEventLoopGroup的构造方法中的逻辑:
1. 对线程数进行确定。如果没有指定线程数后者指定为0,则会将其修改为cpu核心数*2。
2.创建并初始化事件循环NioEventLoop >事件循环组中的每个元素为事件循环,在底层是通过一个名为children的数组(大小为传入的线程数)来保存事件循环的,这里children虽然是EventExecutor类型,但是我们实际使用NioEventLoopGroup的时候,newChild方法生成的children元素为NioEventLoop。

    @Override
    protected EventExecutor newChild(ThreadFactory threadFactory, Object... args) throws Exception {
        return new NioEventLoop(this, threadFactory, (SelectorProvider) args[0],
            ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
    }

   跟踪NioEventLoop的构造方法(下方代码),发现一个NioEventLoop大概有下面这些参数:他所在的EventLoopGroup(parent字段)、对应的NIO中的Selector、选择策略、任务队列、拒绝策略、以及一个Thread实例。关于Executor和Thread是如何工作的,我们后边再去分析。这里稍微注意一下有两个Selector,一个是unwrapped,而另外一个selector(SelectedSelectionKeySetSelector)是对这个unwrapped的封装(装饰者模式),实际它在工作的时候调用的还是内部封装的selector的方法。

    NioEventLoop(NioEventLoopGroup parent, ThreadFactory threadFactory, SelectorProvider selectorProvider,
                 SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
        super(parent, threadFactory, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
        /*省略部分代码*/
        provider = selectorProvider;
        final SelectorTuple selectorTuple = openSelector();
        selector = selectorTuple.selector;
        unwrappedSelector = selectorTuple.unwrappedSelector;
        selectStrategy = strategy;
    }



    protected SingleThreadEventLoop(EventLoopGroup parent, ThreadFactory threadFactory,
                                    boolean addTaskWakesUp, int maxPendingTasks,
                                    RejectedExecutionHandler rejectedExecutionHandler) {
        super(parent, threadFactory, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler);
    }



    protected SingleThreadEventExecutor(
            EventExecutorGroup parent, ThreadFactory threadFactory, boolean addTaskWakesUp, int maxPendingTasks,
            RejectedExecutionHandler rejectedHandler) {
        this.parent = parent;
        this.addTaskWakesUp = addTaskWakesUp;
        thread = threadFactory.newThread(new Runnable() {
              /*省略了run方法*/
        });     
        threadProperties = new DefaultThreadProperties(thread);
        this.maxPendingTasks = Math.max(16, maxPendingTasks);
        taskQueue = newTaskQueue();
        rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
    }

3. 设置chooser。chooser实例具体为PowerOfTwoEventExecutorChooser或者GenericEventExecutorChooser,它的作用就在于选择一个children中的NioEventLoop,这两者的选择方式都是通过一个递增的计数值,选择出对应的children中的元素,具体都是 计数值 % 数组长度,但是当数组长度为2的次幂的时候,取余操作可以替换为 计数值 & (数组长度 - 1)
4. 其他,设置了监听。

  至此,NioEventLoopGroup以及其中的NioEventLoop就创建好了。

  

EventLoop中关联的thread及它是如何工作的

  上文中我们注意到thread这个字段。SingleThreadEventExecutor构造器中指定了thread的run方法究竟要做什么。

    thread = threadFactory.newThread(new Runnable() {
        @Override
        public void run() {
            boolean success = false;
            updateLastExecutionTime();
            try {
                // run方法由SingleThreadEventExecutor的具体子类所实现,
                // 我们采用的是NioEventLoop,所以自然调用的就是NioEventLoop的run方法
                SingleThreadEventExecutor.this.run();
                success = true;
            } catch (Throwable t) {
                logger.warn("Unexpected exception from an event executor: ", t);
            } finally {
                 /*省略部分代码*/
            }
        }
    });

   thread的run方法无非就是在干这个事:

SingleThreadEventExecutor.this.run();

而这个方法我们发现是需要被子类重写的,这里实际执行的就是NioEventLoop重写的run方法。关于run方法做了什么我们先不讲,后边会说。至此线程已经创建好了,但是发现构造器中并没有thread.start()让线程启动,那么线程是什么时候开始启动的呢?SingleThreadEventExecutorexecute(Runnable)方法会启动线程。

    @Override
    public void execute(Runnable task) {
        if (task == null) {
            throw new NullPointerException("task");
        }
        // 判断当前执行线程是否是NioEventLoop关联的线程。
        boolean inEventLoop = inEventLoop();
        if (inEventLoop) {
            addTask(task);
        } else {
            // 启动关联的thread,调用它的start()方法
            startThread();
            // 将要执行的任务添加到任务队列
            addTask(task);
            if (isShutdown() && removeTask(task)) {
                reject();
            }
        }

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

   首先会判断当前执行线程是不是NioEventLoop关联的线程,是的话直接向任务队列中添加Runnable任务,不是的话(此时关联的线程还未启动),会将线程启动并将任务添加到任务队列。

   我们现在已经比较清楚NioEventLoop中关联的线程的创建和启动时机,但是还没有细说它启动后要干嘛,所以我们转向NioEventLoop重写的run方法。

    @Override
    protected void run() {
        for (;;) {
            try {
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));
                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                        // fall through
                    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);
            }
            // Always handle shutdown even if the loop processing threw an exception.
            try {
                if (isShuttingDown()) {
                    closeAll();
                    if (confirmShutdown()) {
                        return;
                    }
                }
            } catch (Throwable t) {
                handleLoopException(t);
            }
        }
    }

   我现在发现NioEventLoop原来是一个死循环,也就是NioEventLoop中所关联的那个thread,一直就在执行run里面的循环。
   我们从整体上来看一下这部分代码。hasTasks方法判断任务队列中是否含有任务(之前提到了当向NioEventLoop添加Runnable任务的时候,任务都会添加到任务队列),如果有任务,则执行selectNow(),没有任务的话执行select(wakenUp.getAndSet(false));(也就是进入case SelectStrategy.SELECT这个分支)

    switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
        case SelectStrategy.CONTINUE:
            continue;
        case SelectStrategy.SELECT:
            select(wakenUp.getAndSet(false));
            if (wakenUp.get()) {
                // wakeUp为true了表明需要唤醒阻塞在selector上的线程让他可以干别的事情
                selector.wakeup();
            }
            // fall through
        default:
    }

当有任务队列有任务的时候,调用selectNow会触发NioEventLoop关联的Selector进行selectNow非阻塞调用(还记得上文说过的unwrappedSelector和它的包装实体selector吗,这里实际就是包装类最终调用的还是真实selector实例unwrappedSelector进行selectNow)。当没有任务的时候,NioEventLoop线程就可以多花点时间用于接收IO事件,可以稍微进行阻塞式的获取,但是又不能一直阻塞着,因为万一碰到任务队列中突然有了任务呢。所以,select(wakenUp.getAndSet(false))中在一定时间内阻塞select IO事件。稍微总结一下switch-case这部分,它也没干什么其他事儿,就是检测有没有IO事件或者有没有任务,这步执行完了之后,就要进行IO事件和任务的处理。

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

   ioTime可以理解为分配给IO事件处理的时间占比,这段代码无非就是分别处理IO事件和任务。
   runAllTasksrunAllTasks(long time)都用于执行任务,只不过后者有时间限制,它们做的就是将可以执行的Scheduled的任务(通过NioEventLoop的schedule方法可以添加定时任务)放入到taskQueue中,然后不断从taskQueue取任务执行。
   processSelectedKeys()用于执行IO事件处理,有两个方法processSelectedKeysOptimizedprocessSelectedKeysPlain,这两个方法看起来不一样,但是内部原理差不多,我在调试的时候一直执行的都是processSelectedKeysOptimized,应该是selectedKeys所指代的对象跟selector实例关联起来了吧。

    private void processSelectedKeysOptimized() {
        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 https://github.com/netty/netty/issues/2363
            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) {
                // null out entries in the array to allow to have it GC'ed once the Channel close
                // See https://github.com/netty/netty/issues/2363
                selectedKeys.reset(i + 1);

                selectAgain();
                i = -1;
            }
        }
    }

   方法里面就是遍历每一个有效的SelectionKey,然后进行事件的处理,final Object a = k.attachment();是从SelectionKey实例中取出attachment,具体的attachment是什么是与netty的通道注册相关的。因为一个SelectionKey认为对应这一个NIO中的SocketChannel,那么这里的attachment取出的就是netty中封装了SocketChannel的那个NioSocketChannel实例(这一部分先了解一下,关于通道注册后面的文章会讲到)。正常情况下会进入processSelectedKey(k, (AbstractNioChannel) a)中执行,点进去会发现,这里面出现了很多Java NIO中比较熟悉的内容,然后在此基础上利用netty的组件(unsafe)进行其他的处理(这个后面会介绍)。

  executor字段代表的Executor实例在执行execute(Runnable)方法时是直接调用ThreadPerTaskExecutor的execute(Runnable)来执行的,最终执行了传入的Runnable对象command的run方法完成具体执行逻辑。ThreadPerTaskExecutor执行execute(Runnable)方法时会创建一个新的线程来完成,所以实现了异步的过程。

    @Override
    public void execute(Runnable command) {
        threadFactory.newThread(command).start();
    }

  我们大概已经了解了EventLoop中的executor是怎么进行工作的(它接收到Runnable任务之后最终创建一个新的线程来执行它),我们再来看看什么时候executor开始工作。找到NioEventLoop的execute方法(实际位于基类SingleThreadEventExecutor),主要代码如下

    private void execute(Runnable task, boolean immediate) {
        // inEventLoop 用于判断 当前实例的thread字段是否等于当前执行线程
        // 第一次进入这个方法的时候由于thread还没有初始化,所以inEventLoop为false
        boolean inEventLoop = inEventLoop();
        // 向任务队列中插入要执行的Runnable任务
        addTask(task);
        if (!inEventLoop) {
            // 如果没有设置当前EventLoop关联的线程thread,那么会进行线程的设置并启动相关逻辑执行
            startThread();
            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) {
                    reject();
                }
            }
        }
        if (!addTaskWakesUp && immediate) {
            wakeup(inEventLoop);
        }
    }

  通过以上的代码我们可以知道,当我们向一个NioEventLoop塞入一个Runnable任务的时候,它会把这个执行任务添加到任务队列TaskQueue中非阻塞的返回,如果当前NioEventLoop还没有绑定线程的时候,会设置线程并启动线程,也就是startThread方法。也就是说当NioEventLoop已经关联到一个线程的时候,每次向它提交一个任务只是简单地非阻塞式的将任务添加到NioEventLoop的任务队列中,方法就返回了。
  我们重点看一下线程启动的时候都在做什么,也就是startThread()。重点在doStartThread中。它会调用所关联的executor来执行一个Runnable任务,由上文可知,executor实例会借助ThreadPerTaskExecutor创建一个新的线程来执行这个Runnable任务,所以thread = Thread.currentThread();这条语句表明当前NioEventLoop关联的Thread实例就是新创建的线程。而Runnable任务所执行的就是被SingleThreadEventExecutor子类(这里就是NioEventLoop)重写的run()方法。

    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();
                try {
                    // 这里的run()方法被SingleThreadEventExecutor子类实例重写,
                    // 例如我们采用的是NioEventLoop,那么这里执行的就是NioEventLoop重写的run方法
                    SingleThreadEventExecutor.this.run();
                    success = true;
                } catch (Throwable t) {
                    logger.warn("Unexpected exception from an event executor: ", t);
                } finally {
                    /*线程结束run方法后的终止逻辑,暂时忽略*/
                }
            }
        });
    }

  

*链接

1. Netty解析:第一个demo——Echo Server
2. Netty解析:NioEventLoopGroup事件循环组
3. Netty解析:NioSocketChannel、NioServerSocketChannel的创建及注册
4. Netty解析:Handler、Pipeline大动脉及其在注册过程中体现
5. Netty解析:connect/bind方法背后
6. Netty解析:服务端如何接受连接并后续处理读写事件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,783评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,360评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,942评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,507评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,324评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,299评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,685评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,358评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,652评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,704评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,465评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,318评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,711评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,991评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,265评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,661评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,864评论 2 335