在Java的NIO中有Selector、Channel、Buffer三大组件。其中Selector组件是用来监听挂载到他上面的Channel的事件,当Selector监听到相关感兴趣的时间之后回交给其他的Selector或是线程/线程池来处理。在Netty中EventLoop以及他的相关类为我们做的就是这个操作,可以简单的理解他整合了Selector以及要执行具体任务的线程池。这句话说得仅仅是一个笼统的概念,具体的实现我们通过先来的文章来娓娓道来。
1、EventLoop初始化源码剖析
EventLoopGroup是一个事件循环组,里面包含一个或多个EventLoop对标与NIO中的Selector。创建EventLoopGroup可以传入一个参数,这个参数表示此Group包含多少个EventLoop,如果要是这个参数为空则会根据机器上CPU的个数进行计算,计算规则为CPU个数乘以2。当作为Boss的EventLoopGroup的EventLoop个数为1或大于一的时候这也就对于与我们BIO模型中的两种细分模型❓
在创建EventLoopGroup的过程中期传递进去的只有一个有关EventLoop个数的参数但是在具体运行的时候程序会通过调用多态构造函数一层层的添加默认的一些参数,这种多态的构造函数一方面简化了使用同时为程序提供了足够大的灵活性,这一点在程序开发的过程中是值得关注的,最后调用的构造函数的各参数意义如下:
public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,
final SelectStrategyFactory selectStrategyFactory) {
super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}
在super中接受的使用采用了不确定入参:
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
这样的设计理念就是为子类的实现提供了更大的空间,子类可以进行更加灵活的入参,父类按照一定的规则来取,扩展父类功能的时候保证对子类的实现不产生影响
- nThreads:Group中EventLoop的个数
- excutor:
- SelectorProvider:可以理解为Selector构造器
- selectStrategyFactory:可以理解为是Selector对于选择SocketChannel的策略,源码中默认的应该是当满足一定的数量后进行拒绝
在有上面入参的接触上结合关于EventLoop、EventLoopGroup这一簇实现我们通过疑问点的方式来梳理一下他实现过程中的关键点:
1、在MultithreadEventExecutorGroup中构建EventLoop数组并且通过newChild创建对应的EventLoop创建EventLoop的过程是在改EventLoop所对应的具体的group中完成的
2、具体创建EventLoop的过程也是一层层调用构造函数的过程,在构造函数调用的过程中完成了对该EventLoop属性的相应赋值。关键的一步是在SingleThreadEventExecutor中完成了创建EventLoopGroup时候传递的selectStrategyFactory的赋值
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");
}
3、NIOEventLoop通过层层关系继承了AbstractEventExecutor,这个类中包含有一个EventExecutorGroup用来记录他多对应的Group
private final EventExecutorGroup parent;
4、根据类图可以知道EventLoop本质上是一类与一个单线程的,这个利用这个单线程执行他所关注的任务,NIOEventLoop继承了SingleThreadEventExecutor,在构造Group的时候我们将传递进去了一个executor,在构造group的时候这个参数默认是null,在SingleThreadEventExecutor中会对他赋默认值。
5、在SingleThreadEventLoop构建的时候会初始化一个DEFAULT_MAX_PENDING_TASKS,这个值是为了后面限制一个EventLoop最多能够关注多少个事件或者执行多少任务(LinkedBlockingQueue);如果添加的事件超过了限制那么就会执行我们的拒绝策略
protected static final int DEFAULT_MAX_PENDING_TASKS = Math.max(16,
SystemPropertyUtil.getInt("io.netty.eventLoop.maxPendingTasks", Integer.MAX_VALUE));
this.maxPendingTasks = Math.max(16, maxPendingTasks);
this.executor = ObjectUtil.checkNotNull(executor, "executor");
taskQueue = newTaskQueue(this.maxPendingTasks);
6、我们知道了EventLoop最终是要执行任务的,在NIOEventLoop中会有一个run的方法这个方法就相当于是一个引子,通过执行一些前置的任务,最终他会通过runAllTasks来执行taskQueue中的一些动作,根据IO操作相关的指标,他会传递进去不同的参数从而来获取一个最佳的执行效果。他根据相关的参数计算一个超时的等待时间
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);
}
}
7、在6中我们可以看到processSelectedKeys()这个方法,此方法中就是调用的Java的nio包中Selector的相关方法,来处理一些他所关注的事件。但是这里处理的应该是读写相关的时间,处理完这些时间之后应该会对任务列表产生一些更新,更新完成之后才会再执行我们的任务队列。至于说读事件应该是在加入队列的时候会判断一下要关注的事件类型
8、6中最终是调用了SingleThreadEventExecutor中的runAllTasks,这里面又会通过for的死循环调用safeExecute,类一个一个执行我们的任务队列中的任务
protected static void safeExecute(Runnable task) {
try {
task.run();
} catch (Throwable t) {
logger.warn("A task raised an exception. Task: {}", task, t);
}
}
2、EventLoop背后的线程模型
在Netty3中只保证了入栈事件是在EventLoop绑定的线程中执行的,当然在这个过程中不需要考虑线程Handler之间的线程同步问题,但是出栈的事件却可能是由其他的线程执行。这样的设计就需要在ChannelHandler中对出站事件进行仔细的同步,这样做的坏处就是增加了编程的复杂度,并且还会带来额外的上下文切换的开销。在Netty4中采用的形式是一个Channel一旦被绑定到了一个EventLoop那么这个Channel所对应的Handler都会被EventLoop绑定的线程执行,这样做避免了线程上下文切换的开销也简化了编程的复杂度。但是在这种场景下所有绑定在同一个EventLoop的Channel他们的LocalThread都是相同的,所以就也无法做一些链路追踪的操作。
3、EventLoop中的关于定时任务的应用
在EventLoop中做的定时任务他所以占用的线程仍旧是EventLoop所对应的线程,所以说在这里还是不能够使用那些耗时的业务,耗时的业务应该通过实操章节的来做,另起一个线程池或者说是一个EventLoopGroup。
new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//根据SocketChannel获取到PipeLine
ch.pipeline()
.addLast(new ServerInHandler1())
//针对Handler整体添加异步处理线程池
.addLast(taskGroup2,new ServerInHandler2())
.addLast("leifHandler",new ServerInLeifHandler());
ScheduledFuture<?> scheduleAtFixedRate = ch.eventLoop().scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("scheduleAtFixedRate所用线程:"+Thread.currentThread().getName());
System.out.println("EventLoop.scheduleAtFixedRate期初延迟1s之后间隔2秒执行");
}
}, 1, 2, TimeUnit.SECONDS);
//添加一个延时任务,这个延时任务应该使用的线程还是EventLoop对应的线程
ch.eventLoop().schedule(new Runnable() {
@Override
public void run() {
System.out.println("schedule所用线程:"+Thread.currentThread().getName());
System.out.println("EventLoop.schedule延迟10秒执行的任务,他的执行将会" +
"取消定时循环执行的任务");
scheduleAtFixedRate.cancel(true);
}
},10, TimeUnit.SECONDS);
}
"取消定时循环执行的任务");
scheduleAtFixedRate.cancel(true);
}
},10, TimeUnit.SECONDS);
}