最近『Java並行処理プログラミング ―その「基盤」と「最新API」を究める―』で Java の並行処理プログラミングを勉強中なんですが、ExecutorService の使い方あたりで完全に消化不良に陥ってしまったので、知識の整理を敢行。 今回は ExecutorService を使ってタスクを実行する方法を見ていきます。 『Java並行処理プログラミング ―その「基盤」と「最新API」を究める―』の第II部を参考にしてます。
ちなみに、この記事(と以降の関連記事)では、「タスク」とは
のどちらかを指して使っています。 たまに java.util.concurrent.Future オブジェクトを、対応するタスクと同一視して指している場合もあります。
記事内容
- ExecutorService とタスクのライフサイクル
- ExecutorService インターフェース
- タスクを依頼・実行するメソッド
ExecutorService とタスクのライフサイクル
ExecutorService は並行処理や非同期処理を行う目的で用いられ、タスク実行の依頼 (submission) と実際のタスク実行 (execution) を分離します。 これに関連して、タスクのライフサイクル(ExecutorService のライフサイクルではない!)には次の4つの段階があります:
- 生成 (created)
- 依頼 (submitted)
- 開始 (started)
- 完了 (completed)
これは次のような図で考えると分かりやすいかと思います(いろいろ先走って図に記入してありますが):
ExecutorService がタスクを保持する(タスクのライフサイクルの)段階は、処理が依頼されているがまだ実行されていない「依頼段階」と、処理が実行されている「開始段階」の2つです。
簡単のため、タスクの処理中に例外が発生せず、タスクのキャンセルも(インターラプトも)されず、ExecutorService のシャットダウンもされないとしましょう*1。 このとき、タスクは次のようにライフサイクルの段階を経ていきます:
- 生成 ⇒ 依頼 : ユーザー(タスク依頼者)が ExecutorService オブジェクトのメソッドを呼び出してタスク実行の依頼をする
- 依頼 ⇒ 開始 : ExecutorService オブジェクトが実行ポリシーにそって、依頼されているタスクを実行に移す
- 開始 ⇒ 完了 : 実行されていたタスクの処理が終了する
これらの中で、ユーザーが制御できる操作は最初の「生成 ⇒ 依頼」の部分だけです。 「実行ポリシー」は ExecutorService のインスタンスとして何を用いるかによって決めます。 これはそのうちに。 以降では、依頼の方法を見ていきましょう。
ExecutorService インターフェース
ExecutorService インターフェースに定義されているタスクの依頼・実行関連のメソッドには以下の通り:
package java.util.concurrent; public interface ExecutorService extends Executor{ @Override void execute(Runnable command); Future<?> submit(Runnable task); <T> Future<T> submit(Runnable task, T result); <T> Future<T> submit(Callable<T> task); <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException; <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException; <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException; <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
- execute() メソッドは ExecutorService のスーパーインターフェースである Executor に定義されています
- execute() メソッドは返り値を扱えない(Future を返さない) submit() メソッドと思っていいでしょう(実装に依るかと思いますが)
- Runnable を引数にとる submit() メソッドは Callable を引数にとるものとあまり変わらないので省略*2
- invokeAll() メソッドと invokeAny() メソッドにはタイムアウトを指定できるメソッドがありますが、当面は無視します
タスクを依頼・実行するメソッド
では、タスクを依頼・実行する ExecutorService のメソッドをそれぞれ見ていきましょう。
- execute(), submit() メソッド
- invokeAll() メソッド
- invokeAny() メソッド
に分けてみていきます。
execute(), submit() メソッド
execute(), submit() メソッドはタスク処理を依頼だけして、制御を呼び出し側に返します。 execute() メソッドは返り値がありませんが、submit() メソッドは Future オブジェクトを返します。 たとえタスクが返り値を返さない場合でも、そのタスクの処理中に例外が発生したかどうかや、タスクがキャンセルされたかどうかなどの情報が Future オブジェクトを介して取得できるので、execute() メソッドよりも submit() メソッドを使う方が無難でしょう。
submit() メソッドによってタスクを依頼した直後、ExecutorService は以下のようになります:
大抵の場合、既に依頼されているタスクや開始されているタスクがあるので、ここでは図にそれらを描き込んでいます。
invokeAll() メソッド
invokeAll() メソッドは1つ以上のタスク処理を依頼し、処理が完了して結果が得られるまで呼び出し側を待たせます。 依頼した全てのタスク処理が完了すれば、それらの返り値を持つ Future オブジェクトを List として返します。 既に結果となるオブジェクトが計算されているのに Future オブジェクトが返されるという点に注意。 submit() メソッドの場合と同様、例外が発生していたりタスクがキャンセルされていたりした場合に Future を介してそれらにアクセスできます。 ちなみに、処理が完了してから制御が返されるので、これらの Future オブジェクトの isDone() メソッドは常に true を返します。
invokeAll() メソッドの呼び出し直後と、このメソッドから制御が返された直後の ExecutorService を図示すると以下のようになります:
invokeAny() メソッド
invokeAny() メソッドは invokeAll() メソッドと同じように1つ以上のタスク処理を依頼し、invokeAll() メソッドと違って1つのタスクの処理が正常に終わるまで呼び出し側を待たせ、最初に得られた結果を返します。 例えば、何かしらの結果を得たい場合に、複数の方法でそれらの計算を試み、最初に目的の結果が得られたらそれを返す・・・ってときなどに使えます。 最初に正常終了したタスク以外はキャンセルされます。 また、タスク処理中に例外が投げられても外にそれを投げません(全ての処理が正常に終了しなかった場合は ExecutionException が投げられます)。
invokeAny() メソッドの呼び出し直後と、このメソッドから制御が返された直後の ExecutorService を図示すると以下のようになります:
Java並行処理プログラミング ―その「基盤」と「最新API」を究める―
- 作者: Brian Goetz,Joshua Bloch,Doug Lea
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2006/11/22
- メディア: 単行本
- 購入: 30人 クリック: 442回
- この商品を含むブログ (169件) を見る
- 作者: 荒木飛呂彦
- 出版社/メーカー: 集英社
- 発売日: 2011/09/16
- メディア: 文庫
- 購入: 11人 クリック: 98回
- この商品を含むブログ (29件) を見る