Netty4.x源码分析之EventLoop(一)

引言

上篇文章我们分析了ServerBootstrap的启动这部分源码,从整体的角度初步接触了netty的编码设计的魅力,今天我们来分析一下Netty里面另一个核心角色–EventLoop。用过netty的同学应该都或多或少知道Netty的高性能很大一部分功劳都在EventLoop的设计,尤其是EventLoop对Reactor线程模型的实现,大大提升了Netty的并发能力,接下来我们就来揭开EventLoop的神秘面纱

UNIX网络I/O模型

Netty一直以其优异的性能表现收获粉丝无数,有研究称netty可以撑过单机10w的TPS。如此高的性能自然离不开精良的线程模型设计和UNIX网络I/O编程模型的选用,我们在看源码前先了解下UNIX网了I/O编程模型和Reactor线程模型。

UNIX网络I/O模型本来有五种,这里只列出三种与我们理解Netty源码有关的,即阻塞I/O模型、非阻塞I/O模型、I/O多路复用模型。

阻塞I/O模型 在JDK1.4之前默认使用的就是阻塞I/O模型,阻塞I/O模型所有的文件操作都是阻塞的。我们以套接字接口为例:在进程空间中调用recvfrom,系统调用知道数据包到达并且被复制到应用进程(用户态)的缓冲区或者发生错误时才返回,在返回之前一直等待,进程从调用recvfrom到返回一直被阻塞。了解这一点,我们就知道为什么JDK1.4之前java的网络通信为什么如此为人所诟病了,只要并发量上来,假设因为某些客观原因网络拥堵,很有可能会有大量accept进来的客户端连接会被阻塞,导致大量连接超时。

非阻塞I/O模型 非阻塞I/O模型与阻塞I/O模型的唯一区别就是recvfrom系统调用发出后,如果应用进程的缓存区没有数据,就直接返回一个EWOULDBLOCK的错误,一般都是使用一个线程进行轮询这个状态,看内核是否有数据到来。这个非阻塞I/O模型对于阻塞I/O模型是个进步,但是为每个连接都是用一个线程进行轮询,这会对cpu时间大量的耗费,性能也不高。

I/O多路复用模型 进程通过将一个或多个文件描述符(fd)传递给select/poll系统调用,阻塞在select操作上,虽然都会阻塞,但是可以实现多个fd阻塞在这个select上,这样我们通过一个线程就可以使用select/poll监听多个文件描述符是否处于就绪状态,这个进步是巨大的,一个线程可以监听大量的连接情况,Java的NIO就是使用的I/O多路复用模型,而Netty就是使用了Java的NIO。

Reactor线程模型

Reactor是一个形象的名称,具体下来可以理解为这么一段伪代码 while(true) {selector.select();后续处理…} ,就是不断的select一把,然后对select出来的连接进行对应的业务操作。Reactor模型实质上是一个观察者模式,所有的事件处理器注册在Dispatcher上,只要select出多个连接,并监听感兴趣的事件,Dispatcher就调用这些对应的事件处理器,并且这些操作是异步的。Reactor线程模型有三种:Reactor单线程模型、Reactor多线程模型、主从多线程模型。

Reactor单线程模型
Reactor单线程模型比较简单,就是一个线程把select操作、监听读写事件,处理业务操作全在一个线程内,虽然简单但也强大,因为使用的I/O多路复用模型,除非你的业务逻辑比较耗时,一般体量的业务Reactor单线程模型就足够了。但是一旦并发连接数上来恐怕是hold不住,单线程无法满足大量的消息编解码、消息读取和发送,可能会导致大量的连接超时。基于这些问题演进了Reactor多线程模型。

Reactor多线程模型
Reactor多线程模型为了解决单线程无法满足大量消息编解码,I/O事件的处理,将Accept事件和读写I/O事件区分开,Accept事件单独使用单线程处理,因Accept操作简单,资源占用少,所以单线程足以支撑海量连接的Accept的操作。而I/O读写事件通过一个线程池处理.Reactor多线程模型充分利用了现代计算机多核cpu的特性,大大提升了TCPServer的并发性能。而Netty正是使用了Reactor多线程模型,只不过在Netty中对多线程模型的线程池做了适应性改造,一个连接绑定一个线程,而不是像JDK原生线程池一样通过一个队列接收所有的task,多个线程竞争获取task执行。这样减少了因为同步导致的资源消耗,一定程度上提高了性能。除了Reactor多线程模型,还有一种Reactor主从多线程模型。

Reactor主从多线程模型
其实一般项目使用Reactor多线程模型足够的,目前本人并不太理解这个主从多线程模型的需求,因为Accept操作并不太消耗资源,加上使用的是I/O多路复用模型,一个线程处理足以大量的Accept请求。尽管如此我们还是简单了解一下主从多线程模型吧,有对主从多线程模型理解更深的欢迎交流。

主从多线程模型除了对Accept操作把单线程换成一个线程池之外,其他的和Reactor多线程模型一模一样。应该是觉得可能在Accept阶段如果使用单线程处理会成为一个瓶颈,所以Accept阶段也使用一个线程池处理,查看李林峰老师的Netty权威指南也指明了很多项目有安全需求,比如服务端需要对客户端握手进行安全认证,安全认证的过程是比较消耗性能的,这种场景下单线程处理Accept过程可能存在性能问题。

NioEventLoopGroup初始化

了解了UNIX网络I/O模型和Reactor线程模型之后我们正式开始了解Netty的EventLoop。我们可以回忆一下我们是怎么在启动ServerBootstrap时设置EventLoop的.

this.bossGroup = new NioEventLoopGroup(bossThreadNumber, new DefaultThreadFactory(bossThreadName, true));
        this.workerGroup = new NioEventLoopGroup(workerThreadNumber, new DefaultThreadFactory(workerThreadName, true));

        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(this.bossGroup, this.workerGroup).channel(NioServerSocketChannel.class).childHandler(channelInitializer);

我们创建两个NioEventLoopGroup实例,从名称可以大概猜到这两个是线程池,一个处理三次握手的Accept过程,一般我们称为Boss线程池,一个是处理读写I/O的网络事件,一般我们称为worker线程池。我们先看看这个NioEventLoopGroup的类层次结构
在这里插入图片描述
可以看到NioEventLoopGroup类的继承体系还是比较复杂的,首先继承了MultithreadEventLoopGroup,然后继承MultithreadEventExecutorGroup,AbstractEventExecutorGroup,并且还实现了ScheduledExecutorService接口,我们可以初步判断这个NioEventLoopGroup不仅仅是个线程池可以提交任务执行,还能提交定时任务执行。接下来我们看看NioEventLoopGroup的初始化。
在这里插入图片描述
可以看到NioEventLoop经过自己的构造函数多层调用后拿到了线程数、threadFactory、selectorProvider,selectStrategyFactory和rejectHandler调用了父类MultithreadEventLoopGroup的构造函数。其中线程数和threadFactory我们不用多说大家都清楚,而selectorProvider就是提供selector对象的,而selectStrategyFactory这里用不到先不说。
接下来MultithreadEventLoopGroup又经过层层调用拿到了ThreadPreTaskExecutor实例和DefaultEventExecutorChooserFactory实例,这里ThreadPreTaskExecutor实现了Executor接口,所以ThreadPreTaskExecutor基本实现了线程池的功能–创建线程和启动线程。而这个DefaultEventExecutorChooserFactory则是多线程中选择的负载均衡器,说白了就是线程选择器。最终调用到了MultithreadEventLoopGroup关键的构造函数,我们看看代码

    protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
      //去除无关代码
        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);
            } 
    
        chooser = chooserFactory.newChooser(children);
    }

在这个关键的构造函数里面初始化了nthreads个EventExecutor数组,for循环创建子线程,这个newChild方法是个抽象方法,实现在NioEventLoopGroup中。

@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类中我们可以看到该类也很复杂,查看类层次结构,可以看到它的父类维护了thread的属性,所以我们可以认为NioEventLoop其实是线程包装类,所以我们可以认为这个EventExecutor数组其实是NioEventLoop–线程包装类数组。所以我们已经可以断定这个MultithreadEventLoopGroup其实就是线程池。

NioEventLoop

NioEventLoop类层次结构也很复杂,首先它继承SingleThreadEventLoop,看到类名大概也知道了这个NioEventLoop首先是个单线程包装类,然后SingleThreadEventLoop继承SingleThreadEventExecuto、AbstractScheduledEventExecutor,最上层还实现了ScheduledExecutorService,所以NioEventLoop还实现了scheduled的逻辑可以执行定时调度任务。我们看看类图
在这里插入图片描述
首先我们看看NioEventLoop的构造函数

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的构造函数拿到了selectorProvider和selectStrategy,并赋值给全局变量,最后通过openSelector()方法拿到一个selector对象,并通过selectorTuple包装好。到这我们大概可以知道每个线程持有一个selector对象,联系到上篇文章分析的channel的注册过程,我们应该知道每个线程应该会通过一个忙循环来select一把来监听所有感兴趣的I/O事件,最终通过pipeline把事件传到业务handler中对这些I/O事件进行处理。最后我们看看刚刚说的父类SingleThreadEventExecutor类构造函数。

private final Queue<Runnable> taskQueue;

    private volatile Thread thread;
    @SuppressWarnings("unused")
    private volatile ThreadProperties threadProperties;
    private final Executor executor;
    private volatile boolean interrupted;

    private final Semaphore threadLock = new Semaphore(0);
    private final Set<Runnable> shutdownHooks = new LinkedHashSet<Runnable>();
    private final boolean addTaskWakesUp;
    private final int maxPendingTasks;
    private final RejectedExecutionHandler rejectedExecutionHandler;

    private long lastExecutionTime;

    @SuppressWarnings({ "FieldMayBeFinal", "unused" })
    private volatile int state = ST_NOT_STARTED;
    
protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,
                                        boolean addTaskWakesUp, int maxPendingTasks,
                                        RejectedExecutionHandler rejectedHandler) {
        super(parent);
        this.addTaskWakesUp = addTaskWakesUp;
        this.maxPendingTasks = Math.max(16, maxPendingTasks);
        this.executor = ObjectUtil.checkNotNull(executor, "executor");
        taskQueue = newTaskQueue(this.maxPendingTasks);
        rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");
    }

可以看到它持有了一个thread,并且还在构造函数中new了一个TaskQueue可以接收任务执行,这个TaskQueue也证实了Netty的EventLoop不仅可以执行I/O任务,也可以把业务任务提交到TaskQueue里执行用户非I/O任务的业务任务。

NioEventLoop启动

经过NioEventLoopGroup的初始化分析,我们知道了NioEventLoopGroup是个线程池,具体子线程维护在NioEventLoop中,并且在上篇文章服务端启动分析中我们也知道了eventLoop是在channel注册的时候和channel绑定了,接下来我们来看看NioEventLoop的线程到底是什么时候启动的。看SingleThreadEventEexecutor代码时,很容易就能注意到startThread()这个方法,从方法名就知道,该方法就是启动线程的方法。

private void startThread() {
        if (state == ST_NOT_STARTED) {
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                doStartThread();
            }
        }
    }

    private void doStartThread() {
        assert thread == null;
        executor.execute(new Runnable() {
            @Override
            public void run() {
                thread = Thread.currentThread();
                if (interrupted) {
                    thread.interrupt();
                }
                try {
                    SingleThreadEventExecutor.this.run();
                    success = true;
                } catch (Throwable t) {
                    logger.warn("Unexpected exception from an event executor: ", t);
                    }
        });
    }
//删除无关代码

可以看到SingleThreadEventExecutor内部维护了线程状态state,如果线程还没启动,那么先CAS把状态修改为启动状态,接着调用doStartThread方法,在doStartThread方法中通过子类传上来的executor对象调用execute方法,而这个executor对象其实是ThreadPreTaskExecutor类,execute方法则是通过threadFactory创建线程并启动线程。

public final class ThreadPerTaskExecutor implements Executor {
    private final ThreadFactory threadFactory;

    public ThreadPerTaskExecutor(ThreadFactory threadFactory) {
        if (threadFactory == null) {
            throw new NullPointerException("threadFactory");
        }
        this.threadFactory = threadFactory;
    }

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

接着回到上面SingleThreadEventExecutor的doStartThread方法,线程启动后把当前的线程赋值给thread这个全局变量,接着调用SingleThreadEventExecutor的run方法,而这个方法不出意外是个抽象方法,其具体实现在子类NioEventLoop中

  @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();
                        }
                    default:
                        // fallthrough
                }

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

看到这个run方法,更加证实了之前我们说的eventLoop与selector绑定,通过一个忙循环监听I/O事件并处理的逻辑。那么到现在我们就只剩一个问题了,什么时候调用了这个startThread方法?要解答这个问题,我们还是回顾下Channel注册到Selector上的过程吧。

AbstractBootstrap.initAndRegister() 
	->MultithreadEventLoopGroup.register(Channel channel)
		->SingleThreadEventLoop.register(Channel channel)
			->SingleThreadEventLoop.register(ChannelPromise promise)
				->AbstractChannel.AbstractUnsafe.register(EventLoop eventLoop,ChannelPromise promise) 

在ServerBootstrap.bind的时候,会先初始化Channel和把Channel注册到Selector上,整个调用链如上所示,最终调用的是Unsafe的注册方法,unsafe的注册方法如下

@Override
        public final void register(EventLoop eventLoop, final ChannelPromise promise) {
           //忽略无关代码
            AbstractChannel.this.eventLoop = eventLoop;

            if (eventLoop.inEventLoop()) {
                register0(promise);
            } else {
                try {
                    eventLoop.execute(new Runnable() {
                        @Override
                        public void run() {
                            register0(promise);
                        }
                    });
                } catch (Throwable t) {
                    logger.warn(
                            "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                            AbstractChannel.this, t);
                    closeForcibly();
                    closeFuture.setClosed();
                    safeSetFailure(promise, t);
                }
            }
        }

在这里会先把eventLoop绑定到channel上,然后调用eventLoop.inEventLoop()方法判断当前的线程是否就是该eventLoop持有的线程,因为是在初始化阶段,eventLoop的线程都还没启动,这里eventLoop.inEventLoop()返回当然是false,所以执行else阶段的代码,eventLoop.execute(new Runnable(){})这个execute是个接口方法,真正实现在SingleThreadEventExecutor类里面。

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

同样,这个execute方法先判断是否为eventLoop自己持有的线程在执行,在这当然是false,然后就来到了else部分,在这会执行两个方法startThread()和addTask(task),还记得上面分析的startThread()么,这就串起来了,通过提交注册channel到selecot上的任务,触发线程的启动。启动之后把刚刚提交的注册任务添加到TaskQueue中,这个注册任务非I/O处理任务,所以提交到TaskQueue中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值