lipermimodその4 IFuture
lipermimodその3の続きになる。ここでは非同期メソッド作成のためのIFutureインターフェースについて説明する。lipermimodについての全投稿は/tag/lipermimodにある。
非同期メソッドとは
Javaの通常のメソッドは当然ながら同期している。メソッドを呼び出すと、呼び出された側で何らかの処理が行われ、それが終了してはじめて呼び出し側に制御が戻ってくる。
しかし、lipermimodでは、メソッド呼び出しが実際には通信であるため、呼び出された側でどんなに早く処理を行っても、通信速度分の遅延は発生するのだが、しかし呼び出し側は常にそれを待たねばならない。
これを解消するための方策としては、呼び出された側の処理の終了を待たずに制御を戻してもらい、処理終了したら、呼び出し側をコールバックしてもらうことである。このやり方は特にlipermimodに限ったことではなく、普通のJavaでも行えることである。例えば以下のように記述できる。
// 重い処理を呼び出す
heavyProc(e-> {
// 後からここが呼び出される
System.out.println("終了した " + e);
});
// すぐにここに戻ってくる
.....
// 重い処理のメソッド
void heavyProc(Consumer<String>consumer) {
// 重い処理を行うスレッドを起動
new Thread() {
public void run() {
// 重い処理
// 終了したと通知
consumer.accept(...);
}
}.start();
// すぐに制御を戻す
}
lipermimodでも全く同じように記述ができる。ただし、もちろんConsumerは使えず、その代わりにIRemoteをextendsしたインターフェースが必要だが。このやり方については、また項を改めて記述することにする。
IFutureのサンプル
前述したように、通常のJavaの場合でもlipermiの場合でも同じようにコールバックによって、重い処理の結果「後から」戻してもらうことができるのだが、lipermimodにはこれとは異なり、もう少し簡単に使える仕組みも提供されている。それがIFutureである。IFutureの機能テストを以下に示す。
import static org.junit.Assert.*;
import java.util.*;
import org.junit.*;
public class IFutureTest {
Server server;
Client client;
List<String>seq = Collections.synchronizedList(new ArrayList<String>());
@Before
public void before() {
server = new Server();
client = new Client();
}
@Test
public void test() throws Exception {
server.registerGlobal("serverGlobal", new ServerGlobalImpl());
server.bind(4455);
client.connect("localhost", 4455);
ServerGlobal serverGlobal = client.getGlobal("serverGlobal");
IFuture<String>future = new IFuture<String>() {
public void returns(String o) {
seq.add("returns " + o);
}
@Override
public void exception(Throwable th) {
seq.add("exception " + th.getMessage());
}
};
seq.add("start");
serverGlobal.hello(future, "ok");
serverGlobal.hello(future, "ng");
seq.add("call finished");
Thread.sleep(1000);
server.close();
assertArrayEquals(new String[] {
"start",
"call finished",
"returns world ok",
"exception ng"
}, seq.toArray(new String[0]));
}
public static interface ServerGlobal extends IRemote {
public String hello(IFuture<String> f, String text);
}
private static class ServerGlobalImpl implements ServerGlobal {
public String hello(IFuture<String> f, String text) {
try {
Thread.sleep(100);
} catch (Exception ex) {}
if (text.equals("ok")) return "world " + text;
throw new RuntimeException("ng");
}
}
}
IFutureの仕組み
仕組みとしては以下のようなものだ。
リモートメソッドの第一引数としてIFutureを指定し、IFutureの型パラメータとメソッド返り値型を同じものにする。
IFutureはコールバックインターフェースなのだが、その返される値を型パラメータとして指定する。メソッド自体の返り値型も同じ型にする必要がある。
public static interface ServerGlobal extends IRemote {
public String hello(IFuture<String> f, String text);
}
IFutureの実装を指定し、メソッドを呼び出す
以下のように呼び出すのだが、制御はすぐに戻ってくる。
そして「思い処理」が終了すると、IFuture#returnsが呼び出されることになる。
IFuture<String>future = new IFuture<String>() {
public void returns(String o) {
seq.add("returns " + o);
}
...
};
serverGlobal.hello(future, "ok");
サーバ側の処理
サーバ側では「重い処理」を行ったら、それを返り値とすればよい。サーバ側に伝えられたIFutureオブジェクトはnullなので、これを呼び出すことはできない。
あくまでも、返り値としたものがクライアント側に渡される。
public String hello(IFuture<String> f, String text) {
return "world " + text;
}
IFutureのメリット
通常のコールバックによる非同期処理に比較するとIFutureによるものは以下のメリットがある。
- IRemoteオブジェクトを処理側に渡す必要がない。IFutureオブジェクトは処理側に渡さず、ローカルで処理されるので、通常のIRemoteオブジェクトの生成と引き渡しが必要無い。
- 処理側はスレッドを使う必要がない。処理にどんなに時間がかかろうとも、処理を別スレッドで行う必要はない。処理結果は単純に返り値として返せばよい。
- 例外について特別な処理を行う必要は無い。返り値と同様に、処理側は例外について特別な処理を行う必要が無い。ごく普通にメソッド中での例外を発生させればよい。