Netty源码分析--Reactor线程模型解析(一)

前言

Reactor线程模型是Netty高性能的关键要素之一。在上一章服务端启动的例子中,我们着重分析了服务端的启动流程。本章我们根据这个例子分析一下Netty的Reactor线程模型,也就是我们用得比较多的NioEventLoopGroup。

大家都知道,如果要基于JDK的API自己实现NIO编程,则需要一个线程池来不断监听端口。接收到新连接之后,这条连接上数据的读写会在另外一个线程池中进行。当然,这个线程池可以复用监听端口的线程池,不过一般不推荐。

在上一章中,bossGroup对应的就是监听端口的线程池,在绑定一个端口的情况下,这个线程池里只有一个线程;workerGroup对应的是连接的数据读写的线程。

那么Netty是如何创建NioEventLoopGroup也就是我们理解的线程池的?又是如何启动线程池里的线程的?线程启动又做了些什么事?这就是本章要分析的内容。

线程组的继承关系

ini 复制代码
NioEventLoopGroup worker = new NioEventLoopGroup();

这行代码创还能了一个工作线程组,这个线程组将负责后续连接的IO事件处理。接下来,我们来看一下NioEventLoopGroup这个对象的类图。

NioEventLoopGroup这个线程组本质上是NioEventLoop线程的集合。我们再来看一下NioEventLoop这个线程的类图。

可以看到,NioEventLoopGroupNioEventLoop的关系实质上就是java中Excutor和Thread对象之间的关系。而NioEventLoop则是继承java中的Thread,并对其进行了一定程度的优化,这个我们下面会讲到。

线程组的初始化

线程组通过构造函数被创建,在创建的过程中不断向上调用父类的构造函数,最终MultithreadEventExecutorGroup这个类的构造方法MultithreadEventExecutorGroup(args...)中完成初始化。我们来看一下这个方法。

ini 复制代码
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) {
        // 1.为executor赋值
        executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
    }

    // 2.初始化children
    children = new EventExecutor[nThreads];

    for (int i = 0; i < nThreads; i ++) {
        boolean success = false;
        try {
            // 3.为children中的每个元素赋值
            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 {
            if (!success) {
                for (int j = 0; j < i; j ++) {
                    children[j].shutdownGracefully();
                }

                for (int j = 0; j < i; j ++) {
                    EventExecutor e = children[j];
                    try {
                        while (!e.isTerminated()) {
                            e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
                        }
                    } catch (InterruptedException interrupted) {
                        // Let the caller handle the interruption.
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }
        }
    }
    // 4.为线程选择器chooser赋值
    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);
}

1.为executor赋值

跟踪代码,可以发现此处executor为null,ThreadPerTaskExecutor(newDefaultThreadFactory())这行代码使用DefaultThreadFactory对象作为参数传入ThreadPerTaskExecutor类的构造函数中。ThreadPerTaskExecutor是java接口Executor的实现类,实现了execute(Runnable command)方法。

typescript 复制代码
    @Override
    public void execute(Runnable command) {
        threadFactory.newThread(command).start();
    }
}

这个方法会通过线程工厂的newThread方法创建线程并执行。此处,该线程工厂即为DefaultThreadFactory这个类对象。我们来看一下这个类的newThread方法

typescript 复制代码
@Override
public Thread newThread(Runnable r) {
    Thread t = newThread(new DefaultRunnableDecorator(r), prefix + nextId.incrementAndGet());
    try {
        if (t.isDaemon()) {
            if (!daemon) {
                t.setDaemon(false);
            }
        } else {
            if (daemon) {
                t.setDaemon(true);
            }
        }

        if (t.getPriority() != priority) {
            t.setPriority(priority);
        }
    } catch (Exception ignored) {
        // Doesn't matter even if failed to set.
    }
    return t;
}

protected Thread newThread(Runnable r, String name) {
    return new FastThreadLocalThread(threadGroup, r, name);
}

private static final class DefaultRunnableDecorator implements Runnable {

    private final Runnable r;

    DefaultRunnableDecorator(Runnable r) {
        this.r = r;
    }

    @Override
    public void run() {
        try {
            r.run();
        } finally {
            FastThreadLocal.removeAll();
        }
    }

可以看到,newThread方法最终返回一个FastThreadLocalThread类的对象,而这个类是继承自java.lang.Thread。

既然说到了FastThreadLocalThread这个类,我们就来讨论一下为什么netty要使用它来作为工作线程,它与java.lang.Thread相比有什么好处。 由于篇幅的限制,我将相关的区别放在了这篇文章里juejin.cn/post/754908...

2.初始化children

children 是一个EventExecutor数组,用于存储该MultithreadEventExecutorGroup管理的所有子事件执行器。每个子事件执行器通常运行在一个独立的线程中,负责处理分配给它的任务。

children 数组的长度等于线程数(nThreads),即该组管理的事件执行器数量。通过 children,可以对这些子执行器进行统一管理和调度,例如关闭、等待终止等操作。

nThreads 是入参中传入的线程数量,默认为cpu逻辑处理器数量的两倍。

3.为children中的每个元素赋值

newchild方法会调用NioEventLoopGroup中的newchild方法

scala 复制代码
public class NioEventLoopGroup extends MultithreadEventLoopGroup {

······

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

······

这个方法会返回EventLoop对象,在前面的类继承关系中可以看到,EventLoopEventExecutor的实现类。下面我们通过时序图来看一下newChild方法干了什么事。

sequenceDiagram autonumber NioEventLoopGroup-)NioEventLoopGroup: newChild(Executor executor, Object... args) NioEventLoopGroup->>NioEventLoop: NioEventLoop(args...) NioEventLoop ->>SingleThreadEventLoop:SingleThreadEventLoop(args...) SingleThreadEventLoop ->>SingleThreadEventExecutor:SingleThreadEventExecutor(args...) SingleThreadEventExecutor ->>AbstractScheduledEventExecutor:AbstractScheduledEventExecutor(args...) AbstractScheduledEventExecutor ->>AbstractEventExecutor:AbstractEventExecutor(args...) AbstractEventExecutor -)AbstractEventExecutor:this.parent = parent
即NioEventLoopGroup对象 SingleThreadEventExecutor -)SingleThreadEventExecutor:设置属性addTaskWakesUp
maxPendingTasks this.executor = executor
taskQueue = newTaskQueue(this.maxPendingTasks)
rejectedExecutionHandler = rejectedHandler SingleThreadEventLoop -)SingleThreadEventLoop:tailTasks = newTaskQueue(maxPendingTasks); NioEventLoop -)NioEventLoop:provider = selectorProvider
selector = openSelector()
selectStrategy = strategy

这张时序图展示了NioEventLoop对象的创建过程,其中有一些关键的步骤值得注意。

  1. newChild传递一个executor参数,这个参数就是前面分析的ThreadPerTaskExecutor,而args参数是我们通过层层调用传递过来的一系列参数。
  2. newChild方法最终创建的是一个NioEventLoop对象,这里的this指的是NioEventLoopGroup,表示归属于哪个NioEventLoopGroup。
  3. 然后,通过调用openSelector方法来创建一个Selector。Selector是NIO编程里最核心的概念,一个Selector可以将多个连接绑定在一起,负责监听这些连接的读写事件,即多路复用。在openSelector方法中,Netty通过反射对Selector底层的数据结构进行了优化。关于openSelector方法,在服务端启动流程中我们分析了具体的类调用关系。
  4. 接下来是TaskQueue = newTaskQueue(this.maxPendingTasks); newTaskQueue()方法在NEventLoop中被重写
NioEventLoop 复制代码
@Override
protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
    // This event loop never calls takeTask()
    return PlatformDependent.newMpscQueue(maxPendingTasks);
}

这里创建的是一个高性能的MPSC队列,也就是多生产者单消费者队列,单消费者指某个NioEventLoop对应的线程,而多生产者就是此NioEventLoop对应的线程之外的线程,通常情况下就是我们的业务线程。比如,我们在调用writeAndFlush的时候,可以不用考虑线程安全,随意调用,这些线程指的就是多消费者,在NioEventLoop的执行部分,我们会详细分析。

如果我们继续往下跟踪,会发现Netty的MPSC队列直接使用的JCTools,可以说Netty的高性能,很大程度上功劳要归功于这个工具包,感兴趣的读者可以了解一下。

关于NioEventLoop的创建,最关键的其实就是两部分:创建一个Selector和创建一个MPSC队列,这三者均为一对一关系。

4.为线程选择器chooser赋值

在传统的NIO编程中,一个新连接被创建后,通常需要给这个连接绑定一个Selector,之后这个连接的整个生命周期都由这个Selector管理。而从上面的代码中,我们分析到,Netty中一个Selector对应一个NioEventLoop,线程选择器的作用正是为一个连接在一个EventLoopGroup中选择一个NioEventLoop,从而将这个连接绑定到某个Selector上。

从前面的类图上可以看到,这里的choserFactory实际上是DefaultEventExecutorChooserFactory。我们来看一下这类的newChooser()方法

typescript 复制代码
@Override
public EventExecutorChooser newChooser(EventExecutor[] executors) {
    if (isPowerOfTwo(executors.length)) {
        return new PowerOfTowEventExecutorChooser(executors);
    } else {
        return new GenericEventExecutorChooser(executors);
    }
}

private static boolean isPowerOfTwo(int val) {
    return (val & -val) == val;
}

可以看到,netty根据线程数是否为2的次幂提供了不同的线程选择器,分别使用累加取模和位运算遍历线程组中的线程。而与运算是操作系统底层支持的,要比取模运算效率高很多。由此可见,在性能优化上,Netty考虑得确实非常细致。

事实上,绝大部分高性能框架都会在细节方面考虑的非常细致,比如mysql的页大小,redis底层的数据结构,就充分利用了操作系统的特性。

小结

关于NioEventLoopGroup的创建,核心知识已经梳理完,我们来稍作总结。

1.在默认情况下,NioEventLoopGroup会创建两倍CPU核数个NioEventLoop,一个NioEventLoop和一个Selector及一个MPSC任务队列一一对应。

2.NioEventLoop线程的命名规则是nioEventLoopGroup-xx-yy,xx表示全局第xx个NioEventLoopGroup,yy表示这个NioEventLoop在NioEventLoopGroup中是第yy个。

3.线程选择器的作用是为一个连接选择一个NioEventLoop,如果NioEventLoop的个数为2的幂,则Netty会使用与运算进行优化。

相关推荐
不知名的前端专家5 小时前
uniapp原生插件 TCP Socket 使用文档
网络·tcp/ip·uni-app·netty
她似晚风般温柔78910 天前
SpringBoot3 + Netty + Vue3 实现消息推送(最新)
java·websocket·spring·vue·netty
广东数字化转型18 天前
LengthFieldBasedFrameDecoder 详细用法
netty·粘包·拆包
探索java23 天前
Netty Channel详解:从原理到实践
java·后端·netty
你打代码的样子真帅24 天前
从零开始构建物联网设备管理系统:基于Netty的高性能IoT平台实战
物联网·netty
9527出列1 个月前
探索服务端启动流程
netty·源码阅读
深圳蔓延科技1 个月前
NioEventLoopGroup 完全指南
netty
深圳蔓延科技1 个月前
如何使用 Netty 实现 NIO 方式发送 HTTP 请求
netty
Derek_Smart1 个月前
Netty 客户端与服务端选型分析:下位机连接场景
spring boot·后端·netty