zoukankan      html  css  js  c++  java
  • Netty源码 reactor 模型

     翻阅源码时,我们会发现netty中很多方法的调用都是通过线程池的方式进行异步的调用,

    这种  eventLoop.execute 方式的调用,实际上便是reactor线程。对应项目中使用广泛的NioEventLoop。还记得我们创建的两个reactor线程池么,具体代码可以参考 Netty源码 服务端的启动

    首先来解释下什么事 reactor 线程

    Reactor模式是处理并发I/O比较常见的一种模式,用于同步I/O,中心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上;
    一旦有I/O事件到来或是准备就绪(文件描述符或socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。

    reactor线程的启动


     io.netty.util.concurrent.SingleThreadEventExecutor#execute

    reactor模型在第一次接受任务的时候,会启动线程

    外部线程在提交任务时,netty会判断是否是当前前程是否是 SingleThreadEventExecutor中的Thread一致,如果不一致,说明需要启动一个新的线程接受任务。然后就会调用内部线程池执行reactor模型的run方法

    然后就会执行addTask将线程封装成一个任务放到Queue中。Queue中的任务就是通过reactor线程来消费的。

    reactor线程的执行


     reactor线程做了三件事

    1.不断的轮训注册到selector上channel的IO事件

    2.处理IO事件,读取channel中的事件,选择一个work线程,准备执行任务

    3.执行任务

    上述三件事不断的轮训,下面我们依次进行分析。

    1.select轮训

    select轮训很简单,分为以下几步

    1.延迟任务队列0.5秒以内有任务则中断

    2.普通任务队列有任务添加则中断

    3.阻塞select操作结束之后,netty又做了一系列的状态判断来决定是否中断本次轮询,中断本次轮询的条件有

    • 轮询到IO事件 (selectedKeys != 0
    • oldWakenUp 参数为true
    • 任务队列里面有任务(hasTasks
    • 第一个定时任务即将要被执行 (hasScheduledTasks()
    • 用户主动唤醒(wakenUp.get()

    4.解决jdk空轮训bug,具体的bug我们可以看 https://www.jianshu.com/p/3ec120ca46b2

    netty这边会记录每次轮训的时间,如果轮训的时间有效,累加器会加1,累加器到256之后,开始rebuildSelector,rebuildSelector的操作其实很简单:new一个新的selector,将之前注册到老的selector上的的channel重新转移到新的selector上

    Select步骤结束,表示轮训到了io事件,那么接下来我们就要去处理这些事件

    2.处理IO事件 

    这里出现了一个selectedKeys,selectedKeys的类型是SelectedSelectionKeySet,其实也就是一个Set集合

    private SelectedSelectionKeySet selectedKeys;

    这里的SelectionKey又是什么呢,我们可以看下类注释

    A selection key is created each time a channel is registered with a selector

    每一个channel在向selector注册时都会创建一个SelectionKey。

    暂且我们先认为 SelectionKey里面包含了通道注册时的一些信息。

    现在我们开始处理io事件

    private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
         for (int i = 0;; i ++) {
             // 1.取出IO事件以及对应的channel
             final SelectionKey k = selectedKeys[i];
             if (k == null) {
                 break;
             }
             selectedKeys[i] = null;
             final Object a = k.attachment();
             // 2.处理该channel
             if (a instanceof AbstractNioChannel) {
                 processSelectedKey(k, (AbstractNioChannel) a);
             } else {
                 NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                 processSelectedKey(k, task);
             }
             // 3.判断是否该再来次轮询
             if (needsToSelectAgain) {
                 for (;;) {
                     i++;
                     if (selectedKeys[i] == null) {
                         break;
                     }
                     selectedKeys[i] = null;
                 }
                 selectAgain();
                 selectedKeys = this.selectedKeys.flip();
                 i = -1;
             }
         }
     }

    这里的k.attachment()能转换成AbstractNioChannel。

    搜一下k.attach的调用关系,在io.netty.channel.nio.AbstractNioChannel#doRegister发现了如下代码,这能解释了selectionKey的创建

    selectionKey = javaChannel().register(eventLoop().selector, 0, this);
    public final SelectionKey register(Selector sel, int ops,
                                           Object att)
            throws ClosedChannelException
        {
            synchronized (regLock) {
                if (!isOpen())
                    throw new ClosedChannelException();
                if ((ops & ~validOps()) != 0)
                    throw new IllegalArgumentException();
                if (blocking)
                    throw new IllegalBlockingModeException();
                SelectionKey k = findKey(sel);
                if (k != null) {
                    k.interestOps(ops);
                    k.attach(att);
                }
                if (k == null) {
                    // New registration
                    synchronized (keyLock) {
                        if (!isOpen())
                            throw new ClosedChannelException();
                        k = ((AbstractSelector)sel).register(this, ops, att);
                        addKey(k);
                    }
                }
                return k;
            }

     我们现在再来看processSelectedKey方法,代码精简后实际上就是调用unsafe对不同的事件进行处理

    3.处理任务队列

    netty中总共有三种任务类型

    1.普通的eventLoop任务

    channel.eventLoop().execute(new Runnable() {
                @Override
                public void run() {
                    //task....
                }
            });

    execute并没有真正去执行,而是将任务进行了封装。

    public void execute(Runnable task) {
            if (task == null) {
                throw new NullPointerException("task");
            }
    
            boolean inEventLoop = inEventLoop();
            if (inEventLoop) {
                addTask(task);
            } else {
                startThread();
                addTask(task);
                if (isShutdown() && removeTask(task)) {
                    reject();
                }
            }
    
            if (!addTaskWakesUp && wakesUpForTask(task)) {
                wakeup(inEventLoop);
            }
        }

    最终我们的任务添加到了一个任务队列中

    protected void addTask(Runnable task) {
            if (task == null) {
                throw new NullPointerException("task");
            }
            if (!offerTask(task)) {
                reject(task);
            }
        }
    
        final boolean offerTask(Runnable task) {
            if (isShutdown()) {
                reject();
            }
            return taskQueue.offer(task);
        }

    这里 taskQueue并不是普通的任务队列,而是Mpsc队列,即多生产者单消费者队列,netty使用mpsc,方便的将外部线程的task聚集,在reactor线程内部用单线程来串行执行

    protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
            // This event loop never calls takeTask()
            return PlatformDependent.newMpscQueue(maxPendingTasks);
        }

    2.外部任务

    服务端在接收到客户端请求时,需要选择相应的channel写数据到客户端

    ctx.channel().writeAndFlush(responsePacket);

    这种在用户线程中的任务最终同样会被封装到任务队列,channel的write最终代码在io.netty.channel.AbstractChannelHandlerContext#write(java.lang.Object, boolean, io.netty.channel.ChannelPromise)

    private void write(Object msg, boolean flush, ChannelPromise promise) {
            AbstractChannelHandlerContext next = findContextOutbound();
            final Object m = pipeline.touch(msg, next);
            EventExecutor executor = next.executor();
            if (executor.inEventLoop()) {
                if (flush) {
                    next.invokeWriteAndFlush(m, promise);
                } else {
                    next.invokeWrite(m, promise);
                }
            } else {
                AbstractWriteTask task;
                if (flush) {
                    task = WriteAndFlushTask.newInstance(next, m, promise);
                }  else {
                    task = WriteTask.newInstance(next, m, promise);
                }
                safeExecute(executor, task, promise, m);
            }
        }

    先判断是否在eventloop线程,这里是false,最终封装成一个task,执行safeExecutor

    private static void safeExecute(EventExecutor executor, Runnable runnable, ChannelPromise promise, Object msg) {
            try {
                executor.execute(runnable);
            } catch (Throwable cause) {
                try {
                    promise.setFailure(cause);
                } finally {
                    if (msg != null) {
                        ReferenceCountUtil.release(msg);
                    }
                }
            }
        }

    接下来就和第一种情况一样,添加到队列当做。

    3.定时任务

    第三种场景就是定时任务逻辑,类似如下

    eventLoop().schedule(new Runnable() {
                                @Override
                                public void run() {
                                    
                            }, connectTimeoutMillis, TimeUnit.MILLISECONDS);

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

     这里的scheduledTaskQueue是一个优先级队列,注意这里的线程安全问题,如果不是在eventloop线程提交的,那么就会把添加操作封装成一个task,这个task的任务是添加[添加定时任务]的任务,而不是添加定时任务,其实也就是第二种场景,这样,对 PriorityQueue的访问就变成单线程,即只有reactor线程

    Queue<ScheduledFutureTask<?>> scheduledTaskQueue() {
        if (scheduledTaskQueue == null) {
            scheduledTaskQueue = new PriorityQueue<ScheduledFutureTask<?>>();
        }
        return scheduledTaskQueue;
    }

    现在再看runAllTasks方法

     分为以下3步

    • 从scheduledTaskQueue转移定时任务到taskQueue
    • 计算本次任务循环的截止时间并执行
    • 执行完成任务后的任务

    代码还是相当清晰的。这里不再深入。

    最后我们再来总结下,reactor模型实质上就干了三件事情,首先他会不停的检测是否有io事件发生或者 是否有任务快要发生,如果检测到了,说明他要去干活了。首先先去处理io事件,所有的io事件都是通过unsafe去处理。处理完io事件后便开始处理任务队列里面的队列。

    以上关于reactor模型的研究。

  • 相关阅读:
    CentOS7- 配置阿里镜像源
    MySQL学习笔记
    OSI&TCP/IP模型
    加密算法学习
    golang学习笔记
    Redis学习总结整理
    TCP
    HTTP/HTTPS
    HTTP2
    MVCC
  • 原文地址:https://www.cnblogs.com/xmzJava/p/10178343.html
Copyright © 2011-2022 走看看