转载 

netty怎样解决NIO空轮询_netty如何解决nio空转问题

分类:    372人阅读    IT小君  2023-09-07 23:15

一、产生的原因

 java的NIO在linux下selector.select()时,本来如果轮询的结果为空并且不调用wakeup的方法的话,这个selector.select()应该是一直阻塞的,但是java却会打破阻塞,继续执行,导致程序无限空转,造成CPU使用率100%
java NIO的空轮询bug 以及Netty的解决办法_第1张图片
这个bug只出现在linux系统下,因为linux下NIO底层使用的是epoll来实现的,而java的epoll实现存在bug,导致selector出现了这种轮询为空却唤醒的情况。windows下NIO是使用的poll来实现selector的就不存在这种bug

Netty中解决该bug的方法

1、设置一个selector.select(timeout),有一个超时时间,selector有4种情况会跳出阻塞

  1. 有事件发生
  2. wakeup
  3. 超时
  4. 空轮询bug

而前两种返回值不为0,可以跳出循环,超时有时间戳记录,所以每次空轮询,有专门 的计数器+1,如果空轮询的次数超过了512次,就认为其触发了空轮询bug。

二、解决办法:

触发bug后,netty直接重建一个selector,将原来的channel重新注册到新的selector上,将旧的 selector关掉

  1. private void select(boolean oldWakenUp) throws IOException {//节选
  2. int selectCnt = 0;
  3. // 计算当前时间
  4. long currentTimeNanos = System.nanoTime();
  5. for(;;)
  6. int selectedKeys = selector.select(timeoutMillis);
  7. selectCnt ++;
  8. if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
  9. // - Selected something,
  10. // - waken up by user, or
  11. // - the task queue has a pending task.
  12. // - a scheduled task is ready for processing
  13. break;
  14. }
  15. long time = System.nanoTime();
  16. if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
  17. // timeoutMillis elapsed without anything selected.
  18. // 超时
  19. selectCnt = 1;
  20. } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
  21. selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {//默认值512
  22. // The code exists in an extra method to ensure the method is not too big to inline as this
  23. // branch is not very likely to get hit very frequently.
  24. // 空轮询一次 cnt+1 如果一个周期内次数超过512,则假定发生了空轮询bug,重建selector
  25. selector = selectRebuildSelector(selectCnt);
  26. selectCnt = 1;
  27. break;
  28. }
  29. }
  30. }
  31. /**
  32. * Replaces the current {@link Selector} of this event loop with newly created {@link Selector}s to work
  33. * around the infamous epoll 100% CPU bug.
  34. * 新建一个selector来解决空轮询bug
  35. */
  36. public void rebuildSelector() {
  37. if (!inEventLoop()) {
  38. execute(new Runnable() {
  39. @Override
  40. public void run() {
  41. rebuildSelector0();
  42. }
  43. });
  44. return;
  45. }
  46. rebuildSelector0();
  47. }
  48. private void rebuildSelector0() {
  49. final Selector oldSelector = selector;
  50. final SelectorTuple newSelectorTuple;
  51. //新建一个selector
  52. newSelectorTuple = openSelector();
  53. // 将旧的selector的channel全部拿出来注册到新的selector上
  54. int nChannels = 0;
  55. for (SelectionKey key: oldSelector.keys()) {
  56. Object a = key.attachment();
  57. if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) {
  58. continue;
  59. }
  60. int interestOps = key.interestOps();
  61. key.cancel();
  62. SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
  63. if (a instanceof AbstractNioChannel) {
  64. // Update SelectionKey
  65. ((AbstractNioChannel) a).selectionKey = newKey;
  66. }
  67. nChannels ++;
  68. }
  69. selector = newSelectorTuple.selector;
  70. unwrappedSelector = newSelectorTuple.unwrappedSelector;
  71. // time to close the old selector as everything else is registered to the new one
  72. //关掉旧的selector
  73. oldSelector.close();
  74. }

1、selector.select(timeoutMillis),调用了select方法,计算一次阻塞产生的时间,并selectCnt ++。

2、获取当前时间,计算select方法的操作时间是否真的阻塞了timeoutMillis,如果是,就证明是一次正常的select(),重置selectCnt = 1;如果不是,就可能触发了JDK的空轮询BUG,然后判断selectCnt 轮询次数是否大于默认的512,是,则说明却是是一直在空轮询,然后进行rebuildSelector()。

3、rebuildSelector()方法重新打开一个Selector;然后遍历oldSelector,将所有的channel重新注册到新的Selector;然后重新赋值selector,selectCnt = 1;这时候已经规避了空轮询。

转载于:https://blog.csdn.net/djydft2831djydft/article/details/113990071

支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者

 工具推荐 更多»