lipermimodその4 IFuture

2019年5月21日

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オブジェクトの生成と引き渡しが必要無い。
  • 処理側はスレッドを使う必要がない。処理にどんなに時間がかかろうとも、処理を別スレッドで行う必要はない。処理結果は単純に返り値として返せばよい。
  • 例外について特別な処理を行う必要は無い。返り値と同様に、処理側は例外について特別な処理を行う必要が無い。ごく普通にメソッド中での例外を発生させればよい。