2012年12月17日月曜日

ExecutorServiceの正しい終了(shutdown)の仕方

ExecutorServiceを使うと簡単にマルチスレッド処理が行えますが、
終了もshutdownメソッドを呼ぶだけと思っていませんか?
shutdownメソッドだけだと不十分です。

学校の先生と生徒の関係で説明します。

shutdownメソッドは指示するだけ
学校で先生が生徒に対して、「問題が解けたら教えてねー」と言います。
生徒は問題が解けた人から「先生できたー!!」と言ってきます。
全員問題が解けた段階で授業が終わるとしましょう。

対応はこう。
先生: ExecutorService
生徒: ExecutorService内のスレッド
問題: ExecutorService.executeに渡すタスク(Runnable)
授業: mainスレッド
学校: Javaプロセス

「問題が解けたら教えてねー」がshutdownメソッド。
「先生できたー!!」が各タスクの完了です。
全部のタスクが完了したらmainスレッド(授業)が終了します。

ここで、とても難しい時間のかかる問題があるとします。当然生徒は問題を解くのにとても時間がかかります。
でも先生(shutdownメソッド)は何もしません。「先生できたー!!」って言ってくるまで「かたくな」に待ちますw
shutdownメソッドは指示するだけだからです。
授業は定時に終わってしまっても(mainスレッドが終わっても)、
学校を閉める事(Javaプロセスを終了する事)はできません。まだ生徒がいるのでww

あなたが先生だとどうしますか?
一定時間経った所で、解き方を教えたり宿題にして授業を終わらせますよね!

時間を決めてタスク完了を待つ
ある一定時間待ってもタスクが終わらない場合、
タスクを中断させることで、ずっとタスク完了を待つことをしないですみます。

awaitTerminationメソッドがこの役割を果たします。

使い方としては、shutdownメソッド実行後にawaitTerminationを実行します。
awaitTerminationの第一引数の時間だけ同期して待ちます。
指定した時間内に全てのタスクが完了した場合、trueを返します。
指定した時間経っても全てのタスクが完了しない場合、falseを返します。

falseを返されるというのは、授業の時間が終わっても、
問題が解けていない生徒がいる状態です。

「解けてない人宿題で明日提出しなさい!!」と言って授業を終わらせましょう!
shutdownNowメソッドがやってくれます。

shutdownNowメソッドは、実行中の全てのスレッドに対してinterruptedを発行して
スレッドを中断します。
そのため、各スレッドではInterruptedExceptionが発生します。(発生するメソッドを使っている場合)

正しい終了の仕方は以下になります。

public static void main(String[] args) {

ExecutorService pool = Executors.newFixedThreadPool(5);
final long waitTime = 8 * 1000;
final long awaitTime = 5 * 1000;

Runnable task1 = new Runnable(){
public void run(){
try {
System.out.println("task1 start");
Thread.sleep(waitTime);
System.out.println("task1 end");
} catch (InterruptedException e) {
System.out.println("task1 interrupted: " + e);
}
}
};

Runnable task2 = new Runnable(){
public void run(){
try {
System.out.println(" task2 start");
Thread.sleep(1000);
System.out.println(" task2 end");
} catch (InterruptedException e) {
System.out.println("task2 interrupted: " + e);
}
}
};
// ある生徒に難しい問題を解かせる
pool.execute(task1);

// 生徒にたくさん問題を解かせる
for(int i=0; i<1000; ++i){
pool.execute(task2);
}

try {
// 「問題が解けたら教えてねー」と生徒に伝える
pool.shutdown();

// 「○○分までにできなかったら全部宿題ねー」と生徒に言って待つ
// (全てのタスクが終了した場合、trueを返してくれる)
if(!pool.awaitTermination(awaitTime, TimeUnit.MILLISECONDS)){
// タイムアウトした場合、全てのスレッドを中断(interrupted)してスレッドプールを破棄する。
pool.shutdownNow();
}
} catch (InterruptedException e) {
// awaitTerminationスレッドがinterruptedした場合も、全てのスレッドを中断する
System.out.println("awaitTermination interrupted: " + e);
pool.shutdownNow();
}

System.out.println("end");
}
waitTimeがawaitTimeより大きい場合、タイムアウトして実行中のタスクが
interruptedすることがわかると思います。
逆にwaitTimeを小さくし、awaitTimeを大きく取ると、綺麗に終了すると思います。

ここで、awaitTimeとshutdownNowメソッドを全てコメントアウトして
shutdownメソッドのみにした場合どうなるか試してみてください。

mainメソッド終了の「end」が出力された後にたくさんtask2のstartとendが
表示されると思います。
これが授業が終わっていても学校を閉じる事ができない状態です。
最悪の場合、Javaプロセスがずっと残ってしまいます。

awaitTerminationとshutdownNowは必ず書くようにしましょう。

shutdownメソッドは重要
awaitTerminationを行なっていればshutdownNowだけでいいような気がしますが違います。
shutdownメソッドは、「それ以上はやらなくていいよ」とも言ってくれます。

shutdownメソッド実行後は、タスクを追加を禁止してくれます。
shutdownメソッド実行後にexecuteメソッドを実行するとRejectedExecutionExceptionが発生します。(submitメソッドなども)

またawaitTerminationは、awaitTerminationメソッド実行時点のタスク完了を待つのではなく、
awaitTerminationメソッド実行後、タスクが全く動いていない状態を監視します。
そのため、shutdownメソッドを実行せずにawaitTerminationを実行しても、
追加タスクによって「タスクが全く動いていない状態」にならない場合、
期待しないawaitTerminationのタイムアウトが発生するかもしれません。

そのため、実行順序としては、

shutdownメソッド
awaitTerminationメソッド
shutdownNowメソッド(例外発生時、タイムアウト時のみ)
となります。

システムによっては「強制的に停止してはいけない」場合もあると思うので、必ずではないですが、基本的にこの順番で実行することをお勧めします。

なお、一定間隔でタスクのスケジューリングができるScheduledExecutorServiceも
同様の順番で終了できますが、 scheduledメソッドを利用した場合はもう少し手順が必要です。

0 件のコメント:

コメントを投稿