Selector.select(timeout)在超时前返回0

时间:2016-03-08 03:43:46

标签: java nio

据Javadoc说,

  

仅在选择了至少一个通道后,才会返回此选择器的唤醒方法,当前线程被中断或给定的超时时间到期,以先到者为准。

但偶尔会在没有任何这四种情况下返回:

  1. 至少选择了一个频道:它返回0
  2. 调用唤醒方法wakeup未调用
  3. 当前线程被中断:Thread.interrupted()返回false
  4. 给定超时期限到期:未按日志过期
  5. 更新2016-03-15

    在第392行和第402行的源代码中,我添加了一些日志: https://github.com/xqbase/tuna/blob/debug/core/src/main/java/com/xqbase/tuna/ConnectorImpl.java

    public boolean doEvents(long timeout) {
        Log.v("Before Select: " + timeout);
        int keySize;
        try {
            keySize = timeout == 0 ? selector.selectNow() :
                    timeout < 0 ? selector.select() : selector.select(timeout);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        Set<SelectionKey> selectedKeys = selector.selectedKeys();
        if (keySize == 0) {
            Log.v("After Select(0): selectedKeys=" + selectedKeys.size() + ", " +
                    "interrupt=" + Thread.interrupted());
            invokeQueue();
            return false;
        }
    
        for (SelectionKey key : selectedKeys) {
            ...
    

    这是日志:

    ...
    2016-03-15 23:07:49.695 com.xqbase.tuna.ConnectorImpl doEvents
    FINE: Before Select: 8120
    2016-03-15 23:07:49.696 com.xqbase.tuna.ConnectorImpl doEvents
    FINE: After Select(0): selectedKeys=0, interrupt=false
    2016-03-15 23:07:49.696 com.xqbase.tuna.ConnectorImpl doEvents
    FINE: Before Select: 8119
    2016-03-15 23:07:49.696 com.xqbase.tuna.ConnectorImpl doEvents
    FINE: After Select(0): selectedKeys=0, interrupt=false
    2016-03-15 23:07:49.700 com.xqbase.tuna.ConnectorImpl doEvents
    FINE: Before Select: 8115
    2016-03-15 23:07:49.701 com.xqbase.tuna.ConnectorImpl doEvents
    FINE: After Select(0): selectedKeys=0, interrupt=false
    2016-03-15 23:07:49.701 com.xqbase.tuna.ConnectorImpl doEvents
    FINE: Before Select: 8114
    2016-03-15 23:07:49.702 com.xqbase.tuna.ConnectorImpl doEvents
    FINE: After Select(0): selectedKeys=0, interrupt=false
    ...
    

    这很奇怪:没有选中的键,没有中断,没有超时和没有唤醒,但它返回了。

    Java中是否存在错误?我的Java版本是1.8.0_51-b16(64位服务器VM),运行在CentOS 6.5 x64 linode上。

2 个答案:

答案 0 :(得分:2)

Javadoc非常清楚。

  

在每次选择操作期间,可以在选择器的选择键组中添加和删除键....选择由select(),select(long)和selectNow()方法执行,包括三个步骤:

     
      
  1. ...

  2.   
  3. 查询基础操作系统,以了解每个剩余通道是否准备好执行其选择操作开始时由其键的兴趣集标识的任何操作。对于已准备好进行至少一个此类操作的通道,将执行以下两个操作之一:

         
        
    1. 如果通道的密钥不在选定密钥集中,则将其添加到该集合中,并修改其就绪操作集以准确识别现在报告该通道准备就绪的那些操作。先前记录在就绪集中的任何就绪信息都将被丢弃。

    2.   
    3. 否则,通道的密钥已经在选定密钥集中,因此修改其就绪操作集以识别报告信道准备就绪的任何新操作。先前记录在就绪集中的任何准备信息都被保留;换句话说,底层系统返回的就绪集按位按顺序分离到密钥的当前就绪集中。

    4.   
  4.   

正在发生的事情是,在返回零的选择中,选择键已经在选定键集中,因此就绪键的数量没有发生变化。

另请注意select(int timeout)方法部分(我的重点):

  

返回:

     
      
  • 键的数量,可能为零,其现成操作集 已更新
  •   

答案 1 :(得分:2)

这可能是JDK中的一个错误。似乎Netty和Mina也遇到了这样的问题,他们重建了选择器作为一种解决方法。

查看最新的Netty代码https://github.com/netty/netty/blob/4.1/transport/src/main/java/io/netty/channel/nio/NioEventLoop.java L641-681:

            if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                // - Selected something,
                // - waken up by user, or
                // - the task queue has a pending task.
                // - a scheduled task is ready for processing
                break;
            }
            ...
            } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                    selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                // The selector returned prematurely many times in a row.
                // Rebuild the selector to work around the problem.
                logger.warn(
                        "Selector.select() returned prematurely {} times in a row; rebuilding selector.",
                        selectCnt);

                rebuildSelector();
                selector = this.selector;

                // Select again to populate selectedKeys.
                selector.selectNow();
                selectCnt = 1;
                break;
            }

参见Mina 2.0代码https://github.com/apache/mina/blob/2.0/mina-core/src/main/java/org/apache/mina/core/polling/AbstractPollingIoProcessor.java L1070-1092:

                if (!wakeupCalled.getAndSet(false) && (selected == 0) && (delta < 100)) {
                    // Last chance : the select() may have been
                    // interrupted because we have had an closed channel.
                    if (isBrokenConnection()) {
                        LOG.warn("Broken connection");
                    } else {
                        LOG.warn("Create a new selector. Selected is 0, delta = " + (t1 - t0));
                        // Ok, we are hit by the nasty epoll
                        // spinning.
                        // Basically, there is a race condition
                        // which causes a closing file descriptor not to be
                        // considered as available as a selected channel,
                        // but
                        // it stopped the select. The next time we will
                        // call select(), it will exit immediately for the
                        // same
                        // reason, and do so forever, consuming 100%
                        // CPU.
                        // We have to destroy the selector, and
                        // register all the socket on a new one.
                        registerNewSelector();
                    }
                }

因此,如果select()返回意外的,则注册新选择器可能是最佳做法。