如何在请求处理线程和SocketChannel选择器线程之间建立一个前后关系?
考虑请求 - 响应协议。如何在请求处理线程和SocketChannel选择器线程之间建立一个前后关系?
我们产生了一个线程来执行select()
循环,用于在已接受的非阻塞SocketChannel
上进行读取和写入操作。这可能看起来像
while (!isStopped()) {
selector.select();
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
SelectionKey selectedKey = selectedKeys.next();
selectedKeys.remove();
Context context = (Context) selectedKey.attachment();
if (selectedKey.isReadable()) {
context.readRequest();
} else /* if (selectedKey.isWritable()) */ {
context.writeResponse();
}
}
}
其中Context
仅仅是相应SocketChannel
容器,缓冲器和逻辑读入并从中写。该readRequest
可能看起来像
public void readRequest() {
// read all content
socketChannel.read(requestBuffer);
// not interested anymore
selectionKey.interestOps(0);
executorService.submit(() -> {
// handle request with request buffer and prepare response
responseBuffer.put(/* some response content */); // or set fields of some bean that will be serialized
// notify selector, ready to write
selectionKey.interestOps(SelectionKey.OP_WRITE);
selectionKey.selector().wakeup(); // worried about this
});
}
换句话说,我们从套接字通道读取,填充一些缓冲和挂断处理一些其他线程。该线程完成处理并准备将其存储在响应缓冲区中的响应。然后通知选择器它想要写入并唤醒它。
Javadoc Selector#wakeup()
没有提及任何发生之前的关系,所以我担心选择器线程可能会看到响应缓冲区(或某个中间对象)处于不一致状态。
这是一种可能的情况?如果是这样的话,有什么正确的方法可以通过Selector
循环线程将响应写入SocketChannel
? (通过一些volatile
字段发布响应?使用SelectionKey
附件?一些其他形式的同步?)
的documentation on Selector
说以下内容:
选择操作同步的选择本身,就按键,并在选择键集中,按照这个顺序。
的happens-before relation在Java Language Specification, Chapter 17定义为包括同步-与关系。
即便如此,您应该在附加的对象上正确同步。这是你的目标,这是你的责任。假设只有您的代码在执行程序的线程中写入responseBuffer
,并且只有选择器线程在之后从中读取,您说您对写入可用性感兴趣,则您有足够的同步。
什么可能会让你惊讶的是,你从interestOps(...)
,即使在wakeup()
之前获得同步。
从我的经验,如果你有一个很难试图实现通过图书馆事业的正确同步(在这种情况下,选择器),你最好同步对象自己,例如关于对象本身的synchronize
声明,ReentrantLock
,您在对象操作中使用的其他一些常见同步对象等等。您失去了一点性能(实际上,在大多数情况下,微不足道,假设您不在被守护部分)保持冷静。
首先,您不需要通知选择器想要写入。你只是写。只有在写入返回零的情况下,才需要涉及选择器或其线程。
其次,由于选择器的三个同步级别,发生了之前发生的关系,前提是您还进行了同步,如下所示。
如果选择器当前正在选择,您的代码可能会在块中调用interestOps()
。这种可能性不会被Javadoc排除。您需要按照正确的顺序执行操作:
- 唤醒。
- 在选择器上同步。
- 致电
interestOps()
。
(2)和选择器自身内部同步的组合建立了任何必要的发生之前的关系。
您目前选择_是什么意思?只要'select()'方法调用还没有返回(当它被阻塞或者正在准备其所有的集合时)?当选择器线程被阻塞时,我无法在'interestOps(..)'中重现阻塞在'select()'中。 – Savior
您的建议顺序会通过'synchronized(选择器)'添加发生前的事件。我假设你建议这样做,因为_选择操作按照选择器本身,按键集和选定键集上的顺序同步._正确吗?如果进入阻塞状态,'select'是否会释放它的'Selector'上的锁?这是记录在任何地方还是我们想要承担?如果'select'线程能够在1和2之间重新调用'select',会发生什么情况。 – Savior
'当前选择'表示与''还没有从'select()'返回。选择器在阻塞时不会释放这三个锁:它不能,因为它们是同步,因此它来自您引用的文本。如果它在1和2之间重新调用select(),则2将等到select()返回下一个。 – EJP
这似乎是[这个问题](https://stackoverflow.com/q/28754275/149138),它也没有很好的答案。我的回答可能是,在实际中,在一个线程上的wakeup()和另一个接收唤醒的线程上的select()调用之间发生了一个事件之前的关系。这有两个实际原因:因为它以任何其他方式工作都会使API非常无用,典型实现的内部机制将涉及相同类型的原子和锁定原语,这些原语会在订购前执行。我认为这是一个文档缺陷。 – BeeOnRope