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的原因。