先々月先月とNIO2の新しいファイルシステムについて解説してきました。今月は、NIO2の残りの機能である非同期I/Oとソケットチャネルでのマルチキャストについて解説していきます。

 なお、ここではNIO2の機能を中心に解説するため、バッファやチャネルなどNIOの機能に関しては特に解説を加えておりません。NIOについては、本連載では2006年の4月から5月にかけて「New I/Oで高速な入出力」と題して解説していますので、そちらをご参照ください。

非同期I/O

通常のI/O

 一般的に入出力処理を行う場合、処理が完了するまで制御が戻ってくることはありません。たとえば、インプットストリームでstream.read(bytes);と記述した場合、読み込みが終了するまでreadメソッドが戻ってくることはありません(例外が発生することはあります)。つまり、処理がブロックされるわけです。

 入出力が高速に行われるのであれば、ブロックされてもそれほど問題にはなりません。しかし、CPUの処理時間に比べると、入出力は桁違いに長い時間を要します。

 このため、入出力のために、システム全体のパフォーマンスが低下しかねません。

 特にWebサーバーなど、多くの入出力を行わなければならないシステムでは、入出力処理によるブロックがパフォーマンスに大きく影響します。

 そこで、従来からブロックしている間に他の処理を行うという手法が使われてきました。たとえば、スレッドプールを使用して並列度を上げることにより、パフォーマンス低下を防ぐ手法などが使用されています。

 たとえば、入力したデータをそのまま出力するサーバーをスレッドプールを使って実装してみましょう。

サンプルのソース (こちらからダウンロードできます)

リスト1●スレッドプールを使ったサーバーの例
    private static final int PORT = 5000;
    private ExecutorService service;
    
    public SimpleEchoServer() throws IOException {
        // スレッドプールの生成
        service = Executors.newCachedThreadPool();
 
        // サーバーソケット
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(PORT));
 
        for (;;) {
            // クライアントからの接続を待つ
            SocketChannel channel = serverChannel.accept();
            System.out.println("Connect to: " 
                               + channel.socket()
                                   .getInetAddress().getHostName());
 
            // クライアントが接続してきたら、
            // ソケットをスレッドに割り当てて入出力処理を行う
            startEcho(channel);
        }
    }
 
    public void startEcho(final SocketChannel channel) {
        // 入出力処理を行うタスク
        Runnable runnable = new Runnable() {
            public void run() {
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                    
                try {
                    for (;;) {
                        // 入力
                        buffer.clear();
                        if (channel.read(buffer) < 0) {
                            break;
                        }
                        
                        // 入力したデータを、そのまま出力
                        buffer.flip();
                        channel.write(buffer);
                    }
                } catch (IOException ex) {
                    ex.printStackTrace();
                } finally {
                    try {
                        channel.close();
                    } catch(IOException ex) {}
                }
            }
        };
 
        // スレッドプールでタスクを実行
        service.execute(runnable);
    }

 クライアントからサーバーに対して接続要求があると、ソケットをスレッドに割り当てて入出力処理を行います。

 上記のコードでは入出力処理を行うタスクをRunnableインタフェースの実装した無名クラスとして定義し(青字部分)、スレッドプールで実行します(赤字部分)。

 このように複数のスレッドを使用することで、ブロックしている間も他の処理を行うことが可能になります。

 しかし、接続が多くなると、その分だけスレッドを使用しなくてはなりません。スレッドが使用できるCPUのコア数よりも多くなると、スレッドの切り替え、つまりコンテキストスイッチが必要になります。

 コンテキストスイッチは比較的重い処理なので、できる限り減らしたいところです。しかし、接続が増えれば増えるほど頻繁にコンテキストスイッチが発生してしまいます。

 つまり性能を向上させるためには、なるべく少ないスレッドで、ブロックする時間を短くする必要があります。

 そこで、登場したのがNIOで導入されたノンブロッキングI/O (I/O多重化)です。

 なお、リスト1のオレンジで示したbindメソッドはJava SE 7で新しく導入されたメソッドです。

 それまではsocketメソッドで対応するjava.net.ServerSocketオブジェクトを取得して、ServerSocketクラスのbindメソッドをコールする必要がありました。しかし、Java SE 7から、java.nio.channels.ServerSocketChannelクラスから直接バインドでできるようになったのです。

 このように細かい部分の改良も、Java SE 7では数多く行われています。