在Netty开发中,一般异步IO事件处理采用NioEventLoopGroup作为线程池,而NioEventLoop为线程池中单个线程,作为系统任务执行单元。接下来将从源码分析Netty是如何处理任务的。
NioEventLoop线程执行入口为其run()方法,其核心业务流程为:
1. select(wakenUp.getAndSet(false));轮询IO事件
2. processSelectedKeys();处理IO事件
3. runAllTasks();处理任务队列
首先来看看轮询IO事件,其调用NioEventLoop中void select(boolean)方法;在void select(boolean),首先计算定时截至时间,在定时超时截至时间之后50ms,依旧没有select()操作,则selectNow()之后跳出循环。
int selectCnt = 0;//统计for循环次数
long currentTimeNanos = System.nanoTime();//当前系统时间
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);//任务截止时间
for (;;) {
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {//超出截至时间50ms
if (selectCnt == 0) {;//循环中还没有发生任何select操作
selector.selectNow();
selectCnt = 1;
}
break;//select之后立即返回
}
…
}
接着判断是否有任务到来,或任务被唤醒,即刻执行selectNow()并返回。
if (hasTasks() && wakenUp.compareAndSet(false, true)) {
selector.selectNow();
selectCnt = 1;
break; }
在既没有达到定时任务截至时间,没有任务,且没有唤醒情形下执行selector.select(timeoutMillis);并将计数selectCnt+1;
执行select(timeoutMillis)再次判断当前状态,检查是否需要退出。
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
// - Selected something,//产生IO事件
// - waken up by user, or//用户唤醒
// - the task queue has a pending task.//taskqueue添加任务
// - a scheduled task is ready for processing//定时任务就绪
break;
}
if (Thread.interrupted()) {
…
selectCnt = 1;
break;
}
接下来解决NIO epoll模型下 selctor状态异常导致cpu负载100%的bug。
在Netty中,死循环select()执行操作时,系统给定一个默认select()有效时间,在反复循环过程中,每次循环时间小于系统给定的有效时间且循环次数到达512时,系统任务进入到epool bug中,接着就进行selector重建。
long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
// TimeUnit.MILLISECONDS.toNanos(timeoutMillis)被认为是一次有效的select()操作
selectCnt = 1;//操作有效将selectCnt重置
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {//当无效循环次数达到阈值512时;
rebuildSelector();//重建selector
selector = this.selector;
selector.selectNow();
selectCnt = 1;
break;
}
// SELECTOR_AUTO_REBUILD_THRESHOLD为设置上限无效循环次数
重建selector核心代码,其将原selector有效key及key.attachment()转移只新建的selector上。
private void rebuildSelector0() {
final Selector oldSelector = selector;
final SelectorTuple newSelectorTuple;
…
newSelectorTuple = openSelector();
…
for (SelectionKey key: oldSelector.keys()) {
Object a = key.attachment();
try {
if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) {
continue;
}
int interestOps = key.interestOps();
key.cancel();
SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
if (a instanceof AbstractNioChannel) {
// Update SelectionKey
((AbstractNioChannel) a).selectionKey = newKey;
}
…
selector = newSelectorTuple.selector
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);
}
}
NioEventLoop主要执行IO时间及定时任务,但是Netty作为高并发IO框架,遵循以优先处理IO任务的原则,ioRatio负载去设置定时任务的执行时间,默认iRatio= 50(及IO时间与任务时间相等)
小结
在轮询IO事件过程中,先判断是否需要退出IO执行流程,或执行轮询IO事件按,并且解决了Epoll bug。
在分析处理IO事件之前,先来说说netty在处理IO事件之前所做的优化。
private SelectorTuple openSelector() {
final Selector unwrappedSelector;
…
unwrappedSelector = provider.openSelector();
…
if (DISABLE_KEYSET_OPTIMIZATION) {
return new SelectorTuple(unwrappedSelector);
}
//默认开启优化
…
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction
上面代码的优化点在NioEventLoop 创建selector时,将其内部成员selectedKeysField和publicSelectedKeysField与selectedKeySet替换,对于selectedKeysField和publicSelectedKeysField而言,它们的数据结构为HashSet,添加的时间复杂读为o(log(n));而selectedKeySet为netty自定义类,它实现的set接口,然而底层却是基于数组的实现,它的添加时间复杂度为o(1);
SelectedSelectionKeySet() {
keys = new SelectionKey[1024];
}
@Override
public boolean add(SelectionKey o) {
if (o == null) {
return false;
}
keys[size++] = o;
if (size == keys.length) {
increaseCapacity();
}
return true;
}
在IO事件触发时,Selector会往底层的selectedKeysField和publicSelectedKeysField添加数据,这种方案可以降低系统运行时间复杂度。
再来看处理IO事件的逻辑。
private void processSelectedKeys() {
if (selectedKeys != null) {//IO事件对应的的selectedkeys,该set已被优化
processSelectedKeysOptimized();//基于系统优化处理IO事件
} else {
processSelectedKeysPlain(selector.selectedKeys());//非优化时间处理
}
}
主要分析基于set优化处理IO事件的函数(没有优化场景下逻辑大致一致)。
private void processSelectedKeysPlain(Set selectedKeys) {
…
Iterator i = selectedKeys.iterator();
for (;;) {
final SelectionKey k = i.next();
final Object a = k.attachment();
…
processSelectedKey(k, (AbstractNioChannel) a);//处理selectorkey
…
if (needsToSelectAgain) {//是否再次执行select()操作
selectAgain();
selectedKeys = selector.selectedKeys();
}
…
i = selectedKeys.iterator();//进入下次循环处理selectedKey相应IO事件
}
}
}
}
从上图截取源代码可以看到,IO事件处理由:执行IO事件和是否更新selectedkeys两大业务处理组成;
展开processSelectedKey(k, (AbstractNioChannel) a)代码可以看到,该代码主要进行读,写,连接等IO事件。
每次执行完IO时间之后,都会检查selectedKeys,其判断条件为needsToSelectAgain;跟着这个变量在elcipse用Ctrl+F搜索,找到该变量的操作如下段所示
void cancel(SelectionKey key) {
key.cancel();
cancelledKeys ++;
if (cancelledKeys >= CLEANUP_INTERVAL) {// CLEANUP_INTERVAL=256
cancelledKeys = 0;
needsToSelectAgain = true;
}
}
可以看到在每次有256次key的取消,needsToSelectAgain = true;便去更新selectedKeys
小结:整个IO事件执行大体逻辑便是,循环去遍历selectedKeys,依次处理相应的IO任务并将selectedKey移除;然后判断是否需要更新selectedKeys(每当有256个key取消任务时,更新)
小结
在处理IO时间时,先对selectedKeys进行优化,然后处理相应IO事件;同时为了避免无效的轮询,在每发生256次selectedKey取消时,重新更新selectedKeys。
处理任务队列的代码位于SingleThreadEventExecutor。
protected boolean runAllTasks(long timeoutNanos) {
fetchFromScheduledTaskQueue();
Runnable task = pollTask();
…
final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
long runTasks = 0;
long lastExecutionTime;
for (;;) {
safeExecute(task);
runTasks ++;
// Check timeout every 64 tasks because nanoTime() is relatively expensive.
if ((runTasks & 0x3F) == 0) {//每执行64次任务检查是否超时
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime >= deadline) {
break;
}
}
task = pollTask();
…
}
…
return true;
}
在处理任务过程中执行fetchFromScheduledTaskQueue();将scheduledTask队列取出并存入taskQueue;为什么Netty开发者要这样做????
我们先来看看这两个列的声明及初始化。
首先看scheduledTask = pollScheduledTask(nanoTime);该方法来自AbstractScheduledEventExecutor,方法返回值为该类成员变量scheduledTaskQueue,初始化过程如下代码:
Queue<ScheduledFutureTask>> scheduledTaskQueue() {
if (scheduledTaskQueue == null) {
scheduledTaskQueue = new PriorityQueue >>();
}
return scheduledTaskQueue;
}
综上可知,scheduledTaskQueue为优先队列,接着来看看taskQueue的初始化与声明:
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");
}
taskQueue为SingleThreadEventExecutor成员变量,在构造器中调用newTaskQueue(this.maxPendingTasks)初始化;
在SingleThreadEventExecutor类成员方法中找到newTaskQueue()实现。
protected Queue newTaskQueue(int maxPendingTasks) {
return new LinkedBlockingQueue(maxPendingTasks);
}
初一看,哦原来taskQueue是一个阻塞队列,似乎感觉有点不妙(NioEventLoop异步执行)!!!!
接着把代码重新定位回到NioEventLoop。
@Override
protected Queue newTaskQueue(int maxPendingTasks) {
// This event loop never calls takeTask()
return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.newMpscQueue(): PlatformDependent.newMpscQueue(maxPendingTasks);
}
在NioEventLoop重写了父类中方法,实际在NioEventLoop执行是taskQueue为newMpscQueue队列;这里才是重点:mpscQueue队列(多生产者单消费者队列)。分析到这里fetchFromScheduledTaskQueue()的工作就是将所有优先队列中数据转移至mpsc队列;Netty要这么做的理由是:为了避免在多线程执行任务队列时,在开发过程考虑同步;这违反了Netty设计核心理念,在线程执行任务时,多个线程同时将任务移交给mscp,而由当前NioEventLoop线程去执行。
(runTasks & 0x3F) == 0,netty设计原则以优先处理IO任务,故在每从任务队列中执行64个任务,将会去检查定时截至时间是否已到,并退出执行任务队列
小结
任务队列的处理主要将任务一直mscp队列中去处理,并在每执行64个任务时,判断是否需要退出循环。