如何在请求处理线程和 SocketChannel 选择器线程之间建立 happens-before 关系?
How to establish a happens-before relationship between a request handling thread and a SocketChannel selector thread?
考虑请求-响应协议。
我们生成一个线程来执行 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
});
}
换句话说,我们从套接字通道读取,填充一些缓冲区并将处理交给其他线程。该线程进行处理并准备一个响应,并将其存储在响应缓冲区中。然后它通知选择器它想要写入并唤醒它。
Selector#wakeup()
的 Javadoc 没有提及任何先行发生关系,因此我担心选择器线程可能会看到响应缓冲区(或某些中间对象)处于不一致状态。
有这种可能吗?如果是,那么通过 Selector
循环线程传递要写入 SocketChannel
的响应的正确方法是什么? (通过某些 volatile
字段发布响应?使用 SelectionKey
附件?某种其他形式的同步?)
首先,您不需要通知选择者要写。你随便写。只有在写入返回零的情况下才需要涉及选择器或其线程。
其次,happens-before 关系是选择器的三级同步的结果,前提是您也进行了同步,如下所示。
如果选择器当前正在选择,您的代码可能会在 interestOps()
调用中 阻塞 。 Javadoc 不排除这种可能性。您需要按照正确的顺序进行操作:
- 醒来。
- 在选择器上同步。
- 呼叫
interestOps()
.
(2) 和选择器自身的内部同步的组合建立了任何必要的先行关系。
documentation on Selector
表示如下:
The selection operations synchronize on the selector itself, on the key set, and on the selected-key set, in that order.
happens-before relation is defined in the Java Language Specification, Chapter 17,包括 与 同步的关系。
即便如此,您也应该在附加对象上正确同步。那是你的目标,这是你的职责。假设只有您的代码在执行程序的线程中写入 responseBuffer
,并且只有选择器线程从中读取 在 您说您对写入可用性感兴趣之后,您有足够的同步。
可能会让您感到惊讶的是,您甚至在 wakeup()
.
之前就从 interestOps(...)
获得了同步
根据我的经验,如果您很难尝试通过库实用程序(在本例中为选择器)实现正确的同步,您最好自己同步您的对象,例如使用对象本身的 synchronize
语句,ReentrantLock
,您在对象操作中使用的其他一些常见同步对象等。您会损失一点点性能(实际上,在大多数情况下微不足道,假设你不要挡在守卫区内)以保持冷静。
考虑请求-响应协议。
我们生成一个线程来执行 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
});
}
换句话说,我们从套接字通道读取,填充一些缓冲区并将处理交给其他线程。该线程进行处理并准备一个响应,并将其存储在响应缓冲区中。然后它通知选择器它想要写入并唤醒它。
Selector#wakeup()
的 Javadoc 没有提及任何先行发生关系,因此我担心选择器线程可能会看到响应缓冲区(或某些中间对象)处于不一致状态。
有这种可能吗?如果是,那么通过 Selector
循环线程传递要写入 SocketChannel
的响应的正确方法是什么? (通过某些 volatile
字段发布响应?使用 SelectionKey
附件?某种其他形式的同步?)
首先,您不需要通知选择者要写。你随便写。只有在写入返回零的情况下才需要涉及选择器或其线程。
其次,happens-before 关系是选择器的三级同步的结果,前提是您也进行了同步,如下所示。
如果选择器当前正在选择,您的代码可能会在 interestOps()
调用中 阻塞 。 Javadoc 不排除这种可能性。您需要按照正确的顺序进行操作:
- 醒来。
- 在选择器上同步。
- 呼叫
interestOps()
.
(2) 和选择器自身的内部同步的组合建立了任何必要的先行关系。
documentation on Selector
表示如下:
The selection operations synchronize on the selector itself, on the key set, and on the selected-key set, in that order.
happens-before relation is defined in the Java Language Specification, Chapter 17,包括 与 同步的关系。
即便如此,您也应该在附加对象上正确同步。那是你的目标,这是你的职责。假设只有您的代码在执行程序的线程中写入 responseBuffer
,并且只有选择器线程从中读取 在 您说您对写入可用性感兴趣之后,您有足够的同步。
可能会让您感到惊讶的是,您甚至在 wakeup()
.
interestOps(...)
获得了同步
根据我的经验,如果您很难尝试通过库实用程序(在本例中为选择器)实现正确的同步,您最好自己同步您的对象,例如使用对象本身的 synchronize
语句,ReentrantLock
,您在对象操作中使用的其他一些常见同步对象等。您会损失一点点性能(实际上,在大多数情况下微不足道,假设你不要挡在守卫区内)以保持冷静。