nio通讯中cpu 100%的问题及处理(转)

本文转自:http://marlonyao.iteye.com/blog/1005690 原文作者:marlonyao

在经典的nio通讯例子中,通常是这样写的:

if (key.isAcceptable()) {  
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();  
                        SocketChannel client = server.accept();  
                        System.out.println("Accepted connection from " + client);  
                        client.configureBlocking(false);  
//开始注册读写事件
                        SelectionKey clientKey = client.register(selector, SelectionKey.OP_WRITE|SelectionKey.OP_READ);  
}
//开始写
if (key.isWritable()) {  
                        // System.out.println("is writable...");  
                        SocketChannel client = (SocketChannel) key.channel();  
                        ByteBuffer buffer = (ByteBuffer) key.attachment();  
                        buffer.flip();  
                        client.write(buffer);  
                        buffer.compact();  
                    }  

在测试例子中,该例子也能够正常的输出,不过就是有一个现象:

但是如果你这时top用看一下发现服务器进程CPU占用到95%以上,如果取消掉32行的注释,服务器会不断地输出"is writable…",这是为什么呢?让我们来分析当第一个客户端连接上时发生什么情况。

  1. 在连接之前,服务器第11行:selector.select()处阻塞。当阻塞时,内核会将这个进程调度至休眠状态,此时基本不耗CPU。
  2.     当客户端发起一个连接时,服务器检测到客户端连接,selector.select()返回。selector.selectedKeys()返回已就绪的SelectionKey的集合,在这种情况下,它只包含一个key,也就是53行注册的acceptable key。服务器开始运行17-25行的代码,server.accept()返回代码客户端连接的socket,第22行在socket上注册OP_READ和OP_WRITE,表示当socket可读或者可写时就会通知selector。
  3.     接着服务器又回到第11行,尽管这时客户端还没有任何输入,但这时selector.select()不会阻塞,因为22行在socket注册了写操作,而socket只要send buffer不满就可以写,刚开始send buffer为空,socket总是可以写,于是server.select()立即返回,包含在22行注册的key。由于这个key可写,所以服务器会运行31-38行的代码,但是这时buffer为空,client.write(buffer)没有向socket写任何东西,立即返回0。
  4.     接着服务器又回到第11行,由于客户端连接socket可以写,这时selector.select()会立即返回,然后运行31-38行的代码,像步骤3一样,由于buffer为空,服务器没有干任何事又返回到第11行,这样不断循环,服务器却实际没有干事情,却耗大量的CPU。

从上面的分析可以看出问题在于我们在没有数据可写时就在socket上注册了OP_WRITE,导致服务器浪费大量CPU资源,解决办法是只有数据可以写时才注册OP_WRITE操作。上面的版本还不只浪费CPU那么简单,它还可能导致潜在的死锁。虽然死锁在我的机器上没有发生,对于这个简单的例子似乎也不大可能发生在别的机器上,但是在对于复杂的情况,比如我写的端口转发工具中就发生了,这还依赖于jdk的实现。

对于上面的EchoServer,出现死锁的场景是这样的:

  1.     假设服务器已经启动,并且已经有一个客户端与它相连,此时正如上面的分析,服务器在不断地循环做无用功。这时用户在客户端输入"hello"。
  2.     当服务器运行到第11行:selector.select()时,这时selector.selectedKeys()会返回一个代表客户端连接的key,显然这时客户端socket是既可读又可写,但jdk却并不保证能够检测到两种状态。如果它检测到key既可读又可写,那么服务器会执行26-38行的代码。如果只检测到可读,那么服务器会执行26-30行的代码。如果只检测到可写,那么会执行31-38行的代码。对于前两种情况,不会造成死锁,因为当执行完29行,buffer会读到用户输入的内容,下次再运行到36行就可以将用户输入内容echo回。但是对最后一种情况,服务器完全忽略了客户端发过来的内容,如果每次selector.select()都只能检测到socket可写,那么服务器永远不能将echo回客户端输入的内容。

避免死锁的一个简单方法就是不要在同一个socket同时注册多个操作。对于上面的EchoServer来说就是不要同时注册OP_READ和OP_WRITE,要么只注册OP_READ,要么只注册OP_WRITE。下面的EchoServer修正了以上的错误:

    public static void main(String[] args) throws IOException {  
        System.out.println("Listening for connection on port " + DEFAULT_PORT);  
      
        Selector selector = Selector.open();  
        initServer(selector);  
      
        while (true) {  
            selector.select();  
      
            for (Iterator<SelectionKey> itor = selector.selectedKeys().iterator(); itor.hasNext();) {  
                SelectionKey key = (SelectionKey) itor.next();  
                itor.remove();  
                try {  
                    if (key.isAcceptable()) {  
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();  
                        SocketChannel client = server.accept();  
                        System.out.println("Accepted connection from " + client);  
                        client.configureBlocking(false);  
                        SelectionKey clientKey = client.register(selector, SelectionKey.OP_READ);  
                        ByteBuffer buffer = ByteBuffer.allocate(100);  
                        clientKey.attach(buffer);  
                    } else if (key.isReadable()) {  
                        SocketChannel client = (SocketChannel) key.channel();  
                        ByteBuffer buffer = (ByteBuffer) key.attachment();  
                        int n = client.read(buffer);  
                        if (n > 0) {  
                            buffer.flip();  
                            key.interestOps(SelectionKey.OP_WRITE);     // switch to OP_WRITE  
                        }  
                    } else if (key.isWritable()) {  
                        System.out.println("is writable...");  
                        SocketChannel client = (SocketChannel) key.channel();  
                        ByteBuffer buffer = (ByteBuffer) key.attachment();  
                        client.write(buffer);  
                        if (buffer.remaining() == 0) {  // write finished, switch to OP_READ  
                            buffer.clear();  
                            key.interestOps(SelectionKey.OP_READ);  
                        }  
                    }  
                } catch (IOException e) {  
                    key.cancel();  
                    try { key.channel().close(); } catch (IOException ioe) { }  
                }  
            }  
        }  
    }  


转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201210260001.html

相关文章:

作者: flym

I am flym,the master of the site:)

发表评论

电子邮件地址不会被公开。 必填项已用*标注