Java NIO SocketChannel.read() 多线程问题

7
我将使用Java NIO实现一个简单的文件服务器,其中包括一个选择线程和多个工作线程(用于执行真正的读/写操作)。
代码的主要部分如下所示:
while (true) {
    int num = selector.select();
    if (num > 0) { 
        Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
        final SelectionKey key = keys.next();
        keys.remove();

        if (key.isValid()) {
            if (key.isAcceptable()) {
                accept(key);
            } else if (key.isReadable()) {
                performReadInWorkerThread (key);
            } else if (key.isWritable()) {
                performWriteInWorkerThread (key);
            }
        }
    }
}

如您从代码片段中所见,当选择一个可读/可写通道时,我会将读/写操作从选择线程转移到工作线程。
现在的问题是,当一个可读通道被移交给工作线程,在它完成/开始从通道读取之前,选择线程再次循环,并且selector.select()选择了先前选择的可读通道(因为通道中仍有输入缓冲区尚未被上一个工作线程完全使用),因此通道又被移交给另一个工作线程,导致多个工作线程读取同一个通道。
我认为这是一个设计问题。我的问题是如何确保同时只有一个线程读取一个通道?

请参考我的答案中的阅读部分,可能会对您有所帮助。 - Amith
如果你想要一个简单的文件服务器,我会使用阻塞式NIO或阻塞式IO。在我看来,这比折腾选择器要简单得多。 - Peter Lawrey
2个回答

7
为什么?读取不会阻塞,可以在当前线程中完成。这样做只会导致无尽的问题。您将不得不在交给读取线程之前注销OP_READ,这很容易,但困难的部分是当读取线程完成读取后,它将不得不重新注册OP_READ,这要求(i)选择器wakeup(),这会导致选择线程在可能没有任何事情可做时运行,这是浪费的,或者(ii)使用挂起的重新注册队列,这会延迟该通道上的下一次读取,直到下一次选择器唤醒,这也是浪费的,否则您必须在添加到队列时立即唤醒选择器,如果没有准备好则也是浪费的。我从未见过一个令人信服的NIO架构,它使用不同的选择和读取线程。
不要这样做。如果您必须进行多线程处理,请将通道组织成组,每个组都有自己的选择器和线程,并且所有这些线程都进行自己的读取。
同样,没有必要在单独的线程中编写。只有在有东西要写入时才进行写入。

谢谢您的解释。那么您的意思是我只需要一个线程吗?如果我必须同时读写非常大的文件,比如有成千上万个客户端请求大文件,我的服务器如何使用单个线程并发地处理这些请求呢? - neevek
@Neevek EJP 是正确的。当你的连接被接受后,每个客户端都必须作为单独的线程运行。 - Amith
1
@Amith 这几乎完全与我所说的相反。这样做会使 NIO 的整个目的失效,该技术的目的是通过节约线程来实现可扩展性。 - user207421
嗨@user486075,我知道nginx可以处理大文件,但我只需要一个简单的文件服务器,它将嵌入在Android应用程序中,是的,它实际上不需要处理大文件。我只是想知道如何一般地在多线程中使用NIO。 - neevek
嗨@user486075,我使用FileChannel.transferTo(),我认为它与sendfile非常相似。但我仍然不明白,如果有很多客户端尝试连接服务器,而服务器(单线程)仍在从其中一个通道读取,因此它无法接受新连接,那么所有这些客户端都将在那里等待,这根本无法扩展。或者我理解错了什么? - neevek
显示剩余3条评论

4

对于NIO,只需记住一个原则: 在主选择线程中进行读/写操作。 这个原则是硬件本质的反映。 不要担心在主选择线程中读取速度不够快。在现代服务器中,CPU总是比网络卡更快。因此,一个线程中的非阻塞读取也比网络卡操作更快。一个线程已经足够读取数据包。我们不需要更多的线程。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接