Java NIO Selector(3)---Selector是如何被唤醒的

        Selector唤醒操作通常是其他线程将迟迟没有返回的sector.select()返回,其实还有一个更省心的做法就是在调用sector.select(timeout),设置一个超时时间,当一定时间内监测的fd上没有没有事件就绪的话,selector.select()就会自动返回。虽然很自动,但是这个时间到底设置多少呢?也是一个值得思考的问题,需要结合具体业务来设置。

        如果不用带有超时时间的selector.select的话,如何将一个selector.select唤醒呢,他又是如何实现的呢?先说一下思路,然后通过源码来验证,我们可以换个角度想这个问题:为什么Selector.seletor不返回?不就是因为他所监测的fd上没有事件就绪嘛。如果他监测的fd上的事件是否就绪,可以让上层应用程序进行控制的话,那么不就可以随时让selector.select返回了吗,但是这是一个很困难的事情,例如,监测一个fd的读事件时,只有对端向这个fd发送了数据,才可以,最主要的在应用程序中这些数据都是真实的业务数据,肯定不能随时随意发送的。如果存在一个和业务无关fd就好了,对于这个fd我们就可以随时发送数据让其相应的事件就绪,在不影响业务的情况下,实现selector.select()的唤醒。还记得上篇文章中说道,每个线程最多可以监测1024个fd,但是实际情况下,最多只能1023个业务fd,其中另外一个就是那个可以让我们随时控制他的事件处于就绪的fd。

特殊的fd

        这个特殊的fd是如何混进到线程监测的这1024个fd当中的呢。第一篇文章中说到,fd和fd感兴趣的事件信息通过pollWrapper传递给底层select的,按道理说这个特殊的fd应该也在pollArray中。是不是这样的呢,看代码。

WindowsSelectorImpl(SelectorProvider sp) throws IOException {
        super(sp);
        pollWrapper = new PollArrayWrapper(INIT_CAP);
        wakeupPipe = Pipe.open();
        wakeupSourceFd = ((SelChImpl)wakeupPipe.source()).getFDVal();

        // Disable the Nagle algorithm so that the wakeup is more immediate
        SinkChannelImpl sink = (SinkChannelImpl)wakeupPipe.sink();
        (sink.sc).socket().setTcpNoDelay(true);
        wakeupSinkFd = ((SelChImpl)sink).getFDVal();

        pollWrapper.addWakeupSocket(wakeupSourceFd, 0);
    }

        在创建WindowSelectorImpl时,将wakeupsourceFd添加到了pollWrapper中索引为0的位置,wakeupsourceFd是什么呢?wakeupsourceFd是从wakeupPipe中获取的一个SelChImpl变量的fd。WakeUpPipe是Pipe.open()产生的。一路跟进Pipe.open最终来到PipeImpl的内部类Initiallizer中。

private class Initializer
        implements PrivilegedExceptionAction<Void>
    {

        private final SelectorProvider sp;

        private IOException ioe = null;

        private Initializer(SelectorProvider sp) {
            this.sp = sp;
        }

        @Override
        public Void run() throws IOException {
            LoopbackConnector connector = new LoopbackConnector();
            connector.run();
            if (ioe instanceof ClosedByInterruptException) {
                ioe = null;
                Thread connThread = new Thread(connector) {
                    @Override
                    public void interrupt() {}
                };
                connThread.start();
                for (;;) {
                    try {
                        connThread.join();
                        break;
                    } catch (InterruptedException ex) {}
                }
                Thread.currentThread().interrupt();
            }

            if (ioe != null)
                throw new IOException("Unable to establish loopback connection", ioe);

            return null;
        }

        Initiallizer类的重点在run方法中,在run方法中调用了LoopbackConnector的run方法。

private class LoopbackConnector implements Runnable {

            @Override
            public void run() {
                ServerSocketChannel ssc = null;
                SocketChannel sc1 = null;
                SocketChannel sc2 = null;

                try {
                    // Create secret with a backing array.
                    ByteBuffer secret = ByteBuffer.allocate(NUM_SECRET_BYTES);
                    ByteBuffer bb = ByteBuffer.allocate(NUM_SECRET_BYTES);

                    // Loopback address
                    InetAddress lb = InetAddress.getByName("127.0.0.1");
                    assert(lb.isLoopbackAddress());
                    InetSocketAddress sa = null;
                    for(;;) {
                        // Bind ServerSocketChannel to a port on the loopback
                        // address
                        if (ssc == null || !ssc.isOpen()) {
                            ssc = ServerSocketChannel.open();
                            ssc.socket().bind(new InetSocketAddress(lb, 0));
                            sa = new InetSocketAddress(lb, ssc.socket().getLocalPort());
                        }

                        // Establish connection (assume connections are eagerly
                        // accepted)
                        sc1 = SocketChannel.open(sa);
                        RANDOM_NUMBER_GENERATOR.nextBytes(secret.array());
                        do {
                            sc1.write(secret);
                        } while (secret.hasRemaining());
                        secret.rewind();

                        // Get a connection and verify it is legitimate
                        sc2 = ssc.accept();
                        do {
                            sc2.read(bb);
                        } while (bb.hasRemaining());
                        bb.rewind();

                        if (bb.equals(secret))
                            break;

                        sc2.close();
                        sc1.close();
                    }

                    // Create source and sink channels
                    source = new SourceChannelImpl(sp, sc1);
                    sink = new SinkChannelImpl(sp, sc2);
                } catch (IOException e) {
                    try {
                        if (sc1 != null)
                            sc1.close();
                        if (sc2 != null)
                            sc2.close();
                    } catch (IOException e2) {}
                    ioe = e;
                } finally {
                    try {
                        if (ssc != null)
                            ssc.close();
                    } catch (IOException e2) {}
                }
            }
        }
    }

        这里的代码虽然多,但是都很清晰:在本机内部建立一个tcp连接,然后在连接上发送一串随机数,然后接收端收到数据后和发送的数据对比,是否相同,如果不同就关闭该tcp连接。如果相同的话,将连接两端的socketChannel分别封装成PipeImpl的SourceChannelImpl类型的成员变量source中和SinkChannelImpl类型的成员变量sink中。

        其中source成员变量绑定的fd,在创建WindeosSelectorImpl中,添加到了pollWrapper的第一个位置中,且该fd感兴趣的事件为读事件。

void addWakeupSocket(int fdVal, int index) {
        putDescriptor(index, fdVal);
        putEventOps(index, Net.POLLIN);
    }

        这样当我们想唤醒监测这个fd的selector时,只需要通过source的另一端sink,向source写入数据即可让source读就绪,从而时selector被唤醒。继续看代码,验证是否是此流程。

        查看WindowsSelectorImpl中的wakeup()代码:

public Selector wakeup() {
        synchronized (interruptLock) {
            if (!interruptTriggered) {
                setWakeupSocket();
                interruptTriggered = true;
            }
        }
        return this;
    }

        继续跟进到setWakeupSocket()中:

private void setWakeupSocket() {
        setWakeupSocket0(wakeupSinkFd);
    }
    private native void setWakeupSocket0(int wakeupSinkFd);

        最终调用了一个本地方法setWakeupSocket0,该方法的参数的确是wakeupSinkFd。到这里基本可以确定是描述的流程。不过,还是看一下setWakeupSocket0的本地实现,在确认一下。

JNIEXPORT void JNICALL
Java_sun_nio_ch_WindowsSelectorImpl_setWakeupSocket0(JNIEnv *env, jclass this,
                                                jint scoutFd)
{
    /* Write one byte into the pipe */
    const char byte = 1;
    send(scoutFd, &byte, 1, 0);
}

        向wakeupSinkFd发送了一个没有实际意义的char类型数据1。让wakeUpSourceFd读事件就绪。进而让selector.select返回。

        同时为了防止下一次selector.select调用时直接返回,要对wakeUpSourceFd的读就绪事件进行及时的消费。

    protected int doSelect(long timeout) throws IOException {
        if (channelArray == null)
            throw new ClosedSelectorException();
        this.timeout = timeout; // set selector timeout
        processDeregisterQueue();
        if (interruptTriggered) {
            resetWakeupSocket();
            return 0;
        }
        // Calculate number of helper threads needed for poll. If necessary
        // threads are created here and start waiting on startLock
        adjustThreadsCount();
        finishLock.reset(); // reset finishLock
        // Wakeup helper threads, waiting on startLock, so they start polling.
        // Redundant threads will exit here after wakeup.
        startLock.startThreads();
        // do polling in the main thread. Main thread is responsible for
        // first MAX_SELECTABLE_FDS entries in pollArray.
        try {
            begin();
            try {
                subSelector.poll();
            } catch (IOException e) {
                finishLock.setException(e); // Save this exception
            }
            // Main thread is out of poll(). Wakeup others and wait for them
            if (threads.size() > 0)
                finishLock.waitForHelperThreads();
          } finally {
              end();
          }
        // Done with poll(). Set wakeupSocket to nonsignaled  for the next run.
        finishLock.checkForException();
        processDeregisterQueue();
        int updated = updateSelectedKeys();
        // Done with poll(). Set wakeupSocket to nonsignaled  for the next run.
        resetWakeupSocket();
        return updated;
    }

        在doSelect中执行updateSelectedKeys后,调用了resetWakeupSocket方法,不出意外,resetWakeupSocket方法就是用来消费wakeupSourceFd上数据的。

private void resetWakeupSocket() {
        synchronized (interruptLock) {
            if (interruptTriggered == false)
                return;
            resetWakeupSocket0(wakeupSourceFd);
            interruptTriggered = false;
        }
    }

    private native void resetWakeupSocket0(int wakeupSourceFd);

        调用resetWakeupSocket0方法,将wakeupSourceFd作为参数传递进去了,

        继续看resetWakeupSocket0方法,这个方法也是一个本地方法:

JNIEXPORT void JNICALL
Java_sun_nio_ch_WindowsSelectorImpl_resetWakeupSocket0(JNIEnv *env, jclass this,
                                                jint scinFd)
{
    char bytes[WAKEUP_SOCKET_BUF_SIZE];
    long bytesToRead;

    /* Drain socket */
    /* Find out how many bytes available for read */
    ioctlsocket (scinFd, FIONREAD, &bytesToRead);
    if (bytesToRead == 0) {
        return;
    }
    /* Prepare corresponding buffer if needed, and then read */
    if (bytesToRead > WAKEUP_SOCKET_BUF_SIZE) {
        char* buf = (char*)malloc(bytesToRead);
        recv(scinFd, buf, bytesToRead, 0);
        free(buf);
    } else {
        recv(scinFd, bytes, WAKEUP_SOCKET_BUF_SIZE, 0);
    }
}

        将指定fd中的数据取出来。

        到此就实现了Selector唤醒操作的实现过程。需要注意的是,在WindowsSelectorImpl的wakeup和resetWakeupSocket方法中,都用到了一个实例变量interruptTriggered,这个变量主要用来标识:selector是否被中断唤醒,更准确来说应该是wakeupSourceFd上的读事件是否被消费了。这变量的作用可以防止重复调用wakeup()和resetWakeupSocket()。同时,如果没有调用selector.select,或者selector.select已经返回了的情况下,调用wakeup的话,会发生什么,就作用而言其实平常情况下调用,并没有区别,只不过当再次调用selector.select时,首先会执行resetWakeupSocket(),并立即返回,这也就是其他地方说的,当调用了2次wakeup后,会影响下一次selector.select的原因。

 

猜你喜欢

转载自blog.csdn.net/weixin_45701550/article/details/102787906