前言
Reactor线程模型是Netty高性能的关键要素之一。在上一章服务端启动的例子中,我们着重分析了服务端的启动流程。本章我们根据这个例子分析一下Netty的Reactor线程模型,也就是我们用得比较多的NioEventLoopGroup。
大家都知道,如果要基于JDK的API自己实现NIO编程,则需要一个线程池来不断监听端口。接收到新连接之后,这条连接上数据的读写会在另外一个线程池中进行。当然,这个线程池可以复用监听端口的线程池,不过一般不推荐。
在上一章中,bossGroup对应的就是监听端口的线程池,在绑定一个端口的情况下,这个线程池里只有一个线程;workerGroup对应的是连接的数据读写的线程。
那么Netty是如何创建NioEventLoopGroup也就是我们理解的线程池的?又是如何启动线程池里的线程的?线程启动又做了些什么事?这就是本章要分析的内容。
线程组的继承关系
ini
NioEventLoopGroup worker = new NioEventLoopGroup();
这行代码创还能了一个工作线程组,这个线程组将负责后续连接的IO事件处理。接下来,我们来看一下NioEventLoopGroup
这个对象的类图。
NioEventLoopGroup
这个线程组本质上是NioEventLoop
线程的集合。我们再来看一下NioEventLoop
这个线程的类图。
可以看到,
NioEventLoopGroup
和NioEventLoop
的关系实质上就是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
对象,在前面的类继承关系中可以看到,EventLoop
是EventExecutor
的实现类。下面我们通过时序图来看一下newChild方法干了什么事。
即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对象的创建过程,其中有一些关键的步骤值得注意。
- newChild传递一个executor参数,这个参数就是前面分析的ThreadPerTaskExecutor,而args参数是我们通过层层调用传递过来的一系列参数。
- newChild方法最终创建的是一个NioEventLoop对象,这里的this指的是NioEventLoopGroup,表示归属于哪个NioEventLoopGroup。
- 然后,通过调用openSelector方法来创建一个Selector。Selector是NIO编程里最核心的概念,一个Selector可以将多个连接绑定在一起,负责监听这些连接的读写事件,即多路复用。在openSelector方法中,Netty通过反射对Selector底层的数据结构进行了优化。关于openSelector方法,在服务端启动流程中我们分析了具体的类调用关系。
- 接下来是
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会使用与运算进行优化。