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模型的研究。

猜你喜欢

转载自www.cnblogs.com/xmzJava/p/10178343.html