使用NIO避免高CPU使用率

时间:2013-02-14 18:10:11

标签: java network-programming cpu-usage

我编写了一个多线程游戏服务器应用程序,它使用NIO处理多个同时连接。不幸的是,只要第一个用户连接,该服务器就会在一个核心上生成完整的CPU负载,即使该用户实际上没有发送或接收任何数据。

以下是我的网络处理线程的代码(缩写为可读性的基本部分)。类ClientHandler是我自己的类,它为游戏机制进行网络抽象。以下示例中的所有其他类均来自java.nio

如您所见,它使用while(true)循环。我的理论是,当密钥可写时,selector.select()将立即返回并调用clientHandler.writeToChannel()。但是当处理程序返回而没有写任何东西时,密钥将保持可写状态。然后立即再次调用select并立即返回。所以我忙着旋转。

只要没有数据要由clientHandler发送,有没有办法以睡眠方式设计网络处理循环?请注意,低延迟对我的用例至关重要,所以当没有处理程序有数据时,我不能让它睡眠任意数量的ms。

ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.socket().bind(new InetSocketAddress(port));
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
// wait for connections

while(true)
{
     // Wait for next set of client connections
    selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> i = keys.iterator();
    while (i.hasNext()) {
        SelectionKey key = i.next();
        i.remove();

        if (key.isAcceptable()) {
            SocketChannel clientChannel = server.accept();
            clientChannel.configureBlocking(false);
            clientChannel.socket().setTcpNoDelay(true);
            clientChannel.socket().setTrafficClass(IPTOS_LOWDELAY);
            SelectionKey clientKey = clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
            ClientHandler clientHanlder = new ClientHandler(clientChannel);
            clientKey.attach(clientHandler);
        }
        if (key.isReadable()) {
            // get connection handler for this key and tell it to process data 
            ClientHandler clientHandler = (ClientHandler) key.attachment();
            clientHandler.readFromChannel();
        }
        if (key.isWritable()) {
            // get connection handler and tell it to send any data it has cached 
            ClientHandler clientHandler = (ClientHandler) key.attachment();
            clientHandler.writeToChannel();
        }
        if (!key.isValid()) {
            ClientHandler clientHandler = (ClientHandler) key.attachment();
            clientHandler.disconnect();
        }
    }
}

2 个答案:

答案 0 :(得分:5)

我没有看到任何理由为什么必须使用相同的选择器进行读写。我会在一个线程中使用一个选择器进行读/接受操作,它将一直阻塞,直到新数据到达。

然后,使用单独的线程和选择器进行写入。您提到在使用可写通道发送消息之前使用缓存来存储消息。在实践中,通道不可写的唯一时间是内核的缓冲区已满,因此它很少是不可写的。实现这一点的一个好方法是拥有一个专用的编写器线程,该线程被赋予消息并且正在休眠;它应该是interrupt()时应该发送新消息,或者在阻塞队列上使用take()。每当有新消息到达时,它将解锁,对所有可写密钥执行select()并发送任何待处理消息;只有在极少数情况下,由于频道不可写,消息必须保留在缓存中。

答案 1 :(得分:5)

SelectionKey clientKey = clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);

问题出在这里。除非套接字发送缓冲区已满,否则SocketChannel几乎总是可写的。因此,他们通常不应注册OP_WRITE:,否则您的选择器循环将旋转。只有在以下情况下才能注册:

  1. 有一些东西要写,
  2. 之前的write()已返回零。