理系学生日記

おまえはいつまで学生気分なのか

Callable と Future

そういえば、ExecutorService#submit が受け取ることができるのは古き良き Runnable に留まらず、Callable も受けとることができる。

この Callable は Runnable#run と似たような call というメソッドを持っているのだけれど、この宣言がイカしていて、Interface Callable に対して、

V call() throws Exception

と定義されている。このプロトタイプから分かるように、特筆すべきは以下の 2 点。

  • スレッドで動作させることができるにも関わらず、値を返すことができる
  • Exception を投げることができる

「値を返すことができるといっても子スレッドとして非同期に動くのだから、親スレッドで join しないといけないんだろ」というのが当然だけれど、ExecutorService#submit に引数として Callable を渡すと、その戻り値として Future が返却される。

<T> Future<T> submit(Callable<T> task)

この Future インタフェースが持つメソッドの抜粋を以下に示す。

boolean isDone();
boolean isCancelled();
V get() throws InterruptedException, ExcecutionException;

これらのメソッドからも分かるように、個々の Future はタスクが完了したかどうかのフラグを持ち、かつ、完了していればその結果を親スレッドに返却することができる。これまで*1子スレッドから親スレッドへの結果返却はかなり直感的でないコーディングをするしかなかったのだけれど、これによってかなり直感的なコードが記述できるようになる。

これらの「例外」、「結果の返却」を実際に試すために、ここでは子スレッドに割り算を実行させてみる。ただし、割り算の前には一律 1 秒の待機時間を設けることにする。

  • CallableExceptionTestMain.java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableExceptionTestMain {

  private static final long SLEEP_TIME = 0;
  
  public static void main(String[] args) {
    ExecutorService executor = Executors.newSingleThreadExecutor();
    List<Future<Double>> futures = new ArrayList<Future<Double>>();
    
    for ( int i = 0; i < 2; i++ ) {
      Devider devider = new Devider(3, 2, 1000l);
      futures.add( executor.submit(devider) );
    }
    Devider devider = new Devider(3, 0, 0);
    futures.add( executor.submit(devider) );
    
    try {
      Thread.sleep( SLEEP_TIME );
    } catch (InterruptedException e1) {
    }
    
    for ( Future<Double> future : futures ) {
      try {
        if ( future.isDone() ) {
          System.out.printf( "result = %6.3f\n", future.get() );
        } 
        else {
          System.out.println( "not yet.");
        }
      } catch (InterruptedException e) {
        e.printStackTrace();
      } catch (ExecutionException e) {
        System.err.println( e.getMessage() );
      }
    }
  }
}
  • Devider.java
import java.util.concurrent.Callable;

public class Devider implements Callable<Double> {

  private long sleepTime = 0;
  private double a, b;
  
  public Devider(double a, double b, long sleep)  { 
    this.a = a;
    this.b = b;
    this.sleepTime = sleep; 
  }
  
  @Override
  public Double call() throws Exception {
    Thread.sleep(sleepTime);
    
    if ( 0 == b ) { throw new IllegalArgumentException("b is 0"); }
    return a/b;
  }
}

まず、タスクを投入した親スレッドが、結果をすぐにもらおうとすると、当然ながら計算は終了していないから、計算結果は一切取得できない。この結果、出力されるのは

not yet.
not yet.
not yet.

となる。
一方で、親スレッドに 4 秒ほど待たせると、計算は当然完了する。ただし、3 つめのスレッドには除数として 0 を渡しており、スレッドは計算時に InvalidArgumentException を返す。この結果、出力は

result =  1.500
result =  1.500
java.lang.IllegalArgumentException: b is 0

となる。

*1:といっても、Java 5.0 には導入されているので、2006 年くらいから?