configureBlocking(false)
前回のコードで、acceptした後にOP_READでselectorにregisterする直前に、以下のような記述がありました。
SocketChannel channel = serverChannel.accept();
channel.configureBlocking(false);
channel.register(key.selector(), SelectionKey.OP_READ);
この「channel.configureBlocking(false)」を実行すると、I/Oがノンブロッキングになります。
しかし前回のecho serverを実際に動かしてみると、クライアントからのリクエストをreadする箇所がノンブロッキングで行われている感触がありませんでした。これは、Selector#select()がクライアントからconnectもしくはsendされるまでの間、このスレッドの処理をブロックしていることに起因します(下図の赤い箇所)。
Non-Blocking I/O
もしreadがノンブロッキングであれば、read待ち中(この図の赤い部分)は何かしら他の処理ができるはずです。しかし、前回のコードではそれが行えませんでした。
これは、以下の2点に起因すると考えています。
- channelがread可能になる契機を、Selectorからの通知でフックしている
- Selectorはchannelの状態に変化があるまでスレッドをブロックする(Selector#select)
1.は、Selector#select以外の何らかの方法でchannelがread可能となる契機をフックできれば良いのだと思いますが、そのような方法を見つけることができませんでした。以下のページによると、Linuxではioctlシステムコールを使って実現できるようです。
2.は、Selector#select()の代わりにSelector#select(long timeout)かSelector#selectNow()を使うことで、ブロッキングを解除することができます。
ということで、今回は2.の部分を変えてノンブロッキングI/Oを実感してみたいと思います。
Do something while waiting a read
前回のコードに対し、Selector#select()の代わりにSelector#select(long timeout)を使うことで、上図の赤い部分に処理を実施するように修正してみました。
1package net.wrap_trap.example.nio;
2
3import java.io.IOException;
4import java.net.InetSocketAddress;
5import java.nio.ByteBuffer;
6import java.nio.channels.ClosedChannelException;
7import java.nio.channels.SelectionKey;
8import java.nio.channels.Selector;
9import java.nio.channels.ServerSocketChannel;
10import java.nio.channels.SocketChannel;
11import java.util.Iterator;
12import java.util.Set;
13
14public class EchoServer {
15
16 private ServerSocketChannel serverChannel;
17 private Selector selector;
18
19 public EchoServer() throws IOException {
20 serverChannel = ServerSocketChannel.open();
21 serverChannel.socket().setReuseAddress(true);
22 serverChannel.socket().bind(new InetSocketAddress(8000));
23
24 serverChannel.configureBlocking(false);
25 selector = Selector.open();
26 serverChannel.register(selector, SelectionKey.OP_ACCEPT);
27 }
28
29 public void start() throws IOException, InterruptedException {
30 System.out.println("start");
31 try {
32 while (true) {
33 int ret = selector.select(10);
34 System.out.println("select loop");
35 if (ret > 0) {
36 handleKeys();
37 } else {
38 System.out.println("do something...");
39 Thread.sleep(10);
40 }
41 }
42 } finally {
43 for (SelectionKey key : selector.keys()) {
44 try {
45 key.channel().close();
46 } catch (IOException ignore) {
47 ignore.printStackTrace();
48 }
49 }
50 }
51 }
52
53 protected void handleKeys() throws IOException, ClosedChannelException {
54 Set<SelectionKey> keys = selector.selectedKeys();
55 for (Iterator<SelectionKey> it = keys.iterator(); it.hasNext();) {
56 SelectionKey key = it.next();
57 it.remove();
58
59 if (key.isAcceptable()) {
60 System.out.println("accept");
61 SocketChannel channel = serverChannel.accept();
62 channel.configureBlocking(false);
63 channel.register(key.selector(), SelectionKey.OP_READ);
64 } else {
65 SocketChannel channel = (SocketChannel) key.channel();
66 if (key.isReadable()) {
67 System.out.println("read");
68 ByteBuffer buffer = ByteBuffer.allocate(4096);
69 int len = channel.read(buffer);
70 if (len > 0) {
71 buffer.flip();
72 channel.write(buffer);
73 } else if (len == -1) {
74 key.cancel();
75 channel.close();
76 }
77 }
78 }
79 }
80 }
81
82 public static void main(String[] args) throws IOException, InterruptedException {
83 EchoServer echoServer = new EchoServer();
84 echoServer.start();
85 }
86}
Selector#select(long timeout)のタイムアウト値は10ミリ秒としました。この値は、Nettyの実装がselect(10)となっていた為、今回参考にしました。10ミリ秒しかブロッキングしないのでCPU使用率が気になるところでしたが、使用率が大きく上昇するようなことはありませんでした。
上記コードでは、「do something…」が表示されている間でも、クライアントから文字列を送ればその文字列が返ってきます。accept待ちやread待ちの状態でも、何か処理を実行できることが実感できます。
実際には、例えばキューにタスクを詰めておいて、I/Oの処理が無い場合にはキューからタスクを取り出して実行する、といった用途なのだと思います。ただしシングルスレッドでそのような仕組みにした場合、キューから取り出したタスクの処理に時間がかかれば、その分echo serverのレスポンスも遅延すると思われます。
Conclusion
nioのノンブロッキングI/Oを実感してみました。上記コードのreadとwriteの間に、何かもう一つノンブロッキングI/Oによる処理を入れると、さらに利便性を感じられるのではないかと考えています。また、時間を見つけてnio2の非同期I/Oも試してみたいと思います。