20130611 java concurrencyinpracticech7
Post on 26-Jun-2015
710 Views
Preview:
TRANSCRIPT
Java並列処理プログラミング 第7章 キャンセルとシャットダウン
2013/6/11 遠山敏章
第7章 キャンセルとシャットダウン
• キャンセルとインタラプションの仕組みの紹介 • タスクやサービスをキャンセルリクエストに対
応付けるプログラムの書き方 • インタラプションとは – あるスレッドが別のスレッドに停止を求めることが
できる仕組み(Thread interrupt()) – Javaにはスレッドを途中で止める仕組みはない
• 瞬時にスレッドを止めたいことはめったにない。 – 後始末をしてから止まるべき
目次
• 7-‐1 タスクのキャンセル – 7-‐1-‐1 インタラプション – 7-‐1-‐2 インタラプションポリシー – 7-‐1-‐3 インタラプションへの応答 – 7-‐1-‐4 例:実行時間の制限 – 7-‐1-‐5 Futureからキャンセルする – 7-‐1-‐6 インタラプトできないブロッキングの扱い方 – 7-‐1-‐7 標準的でないキャンセルを newTaskFor でカプセル化する
• 7-‐2 スレッドを使っているサービスを停止する – 7-‐2-‐1 例:ログ記録サービス – 7-‐2-‐2 ExecutorService のシャットダウン – 7-‐2-‐3 毒薬 – 7-‐2-‐4 例:1回かぎりの実行サービス – 7-‐2-‐5 shutdownNowの制約
• 7-‐3 スレッドの異常終了を扱う – 7-‐3-‐1 未捕捉例外ハンドラ
• 7-‐4 JVMのシャットダウン – 7-‐4-‐1 シャットダウンフック – 7-‐4-‐2 デーモンスレッド – 7-‐4-‐3 ファイナライザ
7-‐1 タスクのキャンセル -‐ 1 • キャンセルする理由 – ユーザーがキャンセルをリクエストした
• GUIアプリのキャンセルボタンをクリック – 時間制限のある活動
• 時間内に最良の結果を返すアプリ – アプリケーションイベント
• 複数のタスクがそれぞれ問題空間を探索しあるタスクが解を見つけたら、他のタスクはキャンセル
– エラー • クローラーのタスクのエラー
– シャットダウン • 穏やかなシャットダウン、緊急のシャットダウン
7-‐1 タスクのキャンセル -‐ 2
• スレッドを強制的に停止する安全な方法はない。
• 協力的な仕組み 1. 「キャンセルがリクエストされた」フラグの周期的
チェック(List 7-‐1) 1. Cancelledをチェック。 2. Cancelled をvolaNleにする
7-‐1 タスクのキャンセル -‐ 3 • List 7-‐2 素数生成クラス
– Sleepのインタラプトが発生してもfinallyで確実にキャンセル • キャンセルされる側のキャンセルポリシー
– How, when, whatを定義 • How: キャンセルをどうやって求めるか • When: キャンセルがリクエストされたことをいつチェックするか • What: キャンセルのリクエストに対してタスクはどんなアクションを実
行すべきか – PrimeGeneratorのキャンセルポリシー
• How: クライアントコードはcancelをコールしてキャンセルをリクエストする
• When: PrimeGeneratorは素数が一つ見つかるたびにキャンセルをチェックする
• What: キャンセルがリクエストされたことを検出したら終了する
7-‐1-‐1 インタラプション
• タスクがキャンセルフラグをチェックせず永久に終わらないケース(List 7-‐3) – BlockingQueueのputでブロックされたスレッドはcancelledフラグをチェックできない
• ブロックするメソッドはインタラプションをサポート(第5章)
• キャンセル以外の目的にインタラプションを使うとプログラムの安定性を損ない、脆弱になる。
7-‐1-‐1 インタラプション -‐ 2 • Threadのインタラプション関連メソッド(List 7-‐4)
– interrupt() • 目的のスレッドをインタラプト
– isInterrupted() • 目的のスレッドのインタラプテッドステータスを返す
– interrupted() • 現在のスレッド(このメソッドを呼んだスレッド)のインタラプテッドステータスをクリアし、その前の値を
返す。 • Thread インタラプションの挙動
– スレッドをブロックしている時、インタラプテッドステータスをクリアし、InterruptedExcepNonを投げる
– スレッドがブロックしていない時、インタラプテッドステータスがセットされ、それを調べるか、調べないかはスレッドの自由。スティキーな状態。
→ interruptメソッドは単に、インタラプションをリクエストしたというメッセージを伝えるだけ。「あなたのご都合のよろしいとき(キャンセルポイント)にお仕事を中断してください」
• Interruptedがtrueなら何かをすべき。 – InterruptedExcepNonを投げる。Interruptを呼び出し、ステータスを復元
7-‐1-‐1 インタラプション -‐ 3
• InterruptedExcepNonを投げる。Interruptを呼び出し、ステータスを復元
• BrokenPrimeProducerはフラグの代わりにインタラプションを使ってキャンセルをリクエスト(List 7-‐5) – 2つのインタラプションのチェック
1. ブロックするputメソッドの中 2. ループの明示的なポーリング
→応答性を上げるために処理の前にインタラプションをチェックするs
7-‐1-‐2 インタラプションポリシー -‐ 1
• スレッドがインタラプションリクエストをどう解釈するかという取り決め
• 例 – インタラプションが検出されたらいつ何をするか – インタラプションに対してはどの仕事単位をアトミック
と見なすべきか – どんなタイミングでインタラプションに応答すべきか
• スレッドはインタラプションポリシーを持つべき。 • 妥当なインタラプションポリシーはスレッドレベル
又は、サービスレベルのキャンセル
7-‐1-‐2 インタラプションポリシー -‐ 2 • インタラプションへの反応はタスクとスレッドで違う • 1つのインタラプトリクエストの目的が複数あることもある
– スレッドプールのワーカースレッドにインタラプトする 1. 現在のタスクをキャンセルせよ 2. このワーカースレッドをシャットダウンせよ
– タスクは自分が所有するスレッドの中では実行されない • サービスからスレッドを借りる • スレッドを所有しないコードはインタラプテッドステータスを保全して
スレッドのオーナーがインタラプションに対応できるように注意すべき。
• インタラプションリクエストの検出時は、実行中の仕事を完了してインタラプションに対応すれば良い
• 単純にInterruptedExcepNonを呼び出し側に広めるのでないならば、interruptedExcepNonをcatchしてからインタラプテッドステータスを復元すべき。:Thread.currentThread().interrupt()
7-‐1-‐3 インタラプションへの応答 -‐ 1
• InterruptedExcepNonの処理 1. 例外を広める(List 7-‐6) 2. インタラプテッドステータスを復元して呼び出し、
スタックの上のほうがそれを処理するようにする • InterruptedEcepNonはもみ消してはいけない。
PrimeProducer(List 7-‐3)でもみ消しているのはスレッドが終了するだけのため。
7-‐1-‐3 インタラプションへの応答 -‐ 2 • ブロックしてインタラプ書んを受け付けるメソッド
を呼び出す活動がキャンセルをサポートしていない場合(List 7-‐7) → インタラプションのステータスをInterruptedExcepNonのcatch後すぐに復元せず、ステータスを保存し、リターン直前に復元すべき – 早くセットすると無限ループになることもあるため。
• コードがブロックしてインタラプションを受け付けるメソッドを呼び出さない場合でも、インタラプテッドステータスをポーリングすることによって、インタラプションへの応答性を持たせられます。
7-‐1-‐4 例:実行時間の制限 -‐ 1 • 無限の時間のかかる問題で「最大10分だけ答えを探
せ」と指定する。 – PrimeGenerator(List 7-‐2)は1秒経つ前にRunNmeExcepNonを投げたら気づかれないままになる。
• 任意のRunnableを一定時間動かす試み(List 7-‐8) – ScheDuleExecutorServiceで一定時間後にキャンセル(taskThread.interrupt())を呼び出して止める
– 例外はNedRunを呼び出す側でcatchできる。 – この方法は反則。スレッドにインタラプションするためには
そのスレッドのインタラプションポリシーを知ってなければ、ならないため。 • タイムアウトの前にタスクが終了したら、Returnした後で、スタート
する。
7-‐1-‐4 例:実行時間の制限 -‐ 2 • 専用スレッドの中でタスクにインタラプトする例(List 7-‐9) – 2つの問題を解決
• aSecondPrimeOfPrimesの例外処理がcatchされなの問題 • List 7-‐8のキャンセルタスクの実行タイミングの問題
– 挙動 • NmedRunは時間制限付きのjoinをその新たに作られたスレッドで
実行。JoinでtaskThreadが終わるまで待つ • 例外が投げられていた場合はNmedRunの呼び出したスレッドの
中で再投する – 欠点
• 制御が現在のスレッドに戻ったのはスレッドが正常終了したのか、joinがタイムアウトしたのか分からない。(Thread APIの欠点)
7-‐1-‐5 Futureからキャンセルする
• Futureを使って、タスクをキャンセルする(List 7-‐10) – taskExec.submitでfutureを返す – task.cancel(false)で実行中ならインタラプトする – タスクがキャンセルより前に例外を投げたら再投
する
7-‐1-‐6 インタラプトできないブロッキングの扱い -‐ 1
• ブロックするメソッドやブロックの仕組みのすべてがインタラプションに応答するとはかぎらない。
• インタラプションに似た方法でストップさせることが可能な場合もありますが、そのためにはスレッドがブロックしている理由に関する詳しい知識が必要。
• 例 – Java.ioの同期ソケットI/O
• ソケットをクローズするとread/writeでブロックしているスレッドはSocketExcepNonを投げる。 – Java.nioの同期I/O
• InterrupNbleChannel上でウェイトしているスレッドにインタラプトするとClosedByInterrptExcepNonを投げてチャネルをクローズする。
– Selectorによる非同期I/O • スレッドがSelector.selectでブロックしていると、wakeupがClosedSelectorExcepNonを投げてselectを途
中でリターン。 – ロックの取得
• スレッドが固有のロック(2-‐3-‐1)を持ってブロックしている時は、そのスレッドをストップするためにできることはない。
• ロックを取得させて処理を進行させ、ほかの方法でスレッドの注意を引くことしかできない。 • Lockを待ちながらインタラプションに応答できるLockクラスのlockInterrupNblyメソッド
7-‐1-‐6 インタラプトできないブロッキングの扱い -‐ 2
• 標準的でないキャンセルをカプセル化するReaderThread(List 7-‐11) – Interruptをオーバーライドして、「標準のインタラ
プトを渡すこと」と「ソケットをクローズすること」の両方をやらせる • Readでブロックしていても、ブロックしてインタラプトを
受け付けるメソッドを呼び出し中でもスレッドを停止できる。
7-‐1-‐7 標準的でないキャンセルをnewTaskForでカプセル化する -‐ 1
• 標準的でないキャンセルをカプセル化したテクニックをThreadPoolExecutorのnewTaskForフックで洗練させる – ExecutorServiceにCallableを依頼するとき、submit
はタスクをキャンセルするために使えるFutureを返します。
– Future.cancel()をオーバーライドするとタスクに対して「ソケットを使うスレッドのキャンセルをカプセル化」(List 7-‐11)するのと同等のことができる
7-‐1-‐7 標準的でないキャンセルをnewTaskForでカプセル化する -‐ 2
• newTaskForでタスクの標準的でないキャンセルをカプセル化する(List 7-‐12) – CancellableTaskインタフェイス
• Callableをextendsしている • CancellingExecutorがThreadPoolExecutorをextendsし、newTaskForをオーバーライトして
CancellableTaskに自分のFutureを作らせている。 – SocketUsingTaskアブストラクトクラス
• CancellableTaskをimplementsする • Future.cancel()を定義してsuper.cancel()に加えソケットをクローズする。
– 挙動 • SocketUsingTaskがFutureからキャンセルされると、ソケットがクローズされ、実行中のス
レッドがインタラプトされます。 – 利点
• キャンセルに対するタスクの応答性が良くなる • キャンセルにたいする応答性を維持できる • ブロックしてインタラプトを受け付けるメソッドを安全に呼び出せる • ブロックするソケットI/Oのメソッドも呼び出せる
7-‐2 スレッドを使っているサービスを停止する
• シャットダウンするときはサービスが所有するスレッドも終わる必要がある。
• カプセル化を壊さないためにはスレッドのオーナーではないコードがスレッドを操作してはいけない。(インタラプト、プライオリティの変更) – スレッドプールはそのワーカースレッドのオーナーなので、
スレッドがインタラプトされたら、スレッドプールが面倒を見るべき。
• スレッドを所有するサービスはスレッドをシャットダウンするライフサイクルメソッドを持つべき。 – ExecutorService
• shutdon(), shutdownNow()
7-‐2-‐1 例:ログ記録サービス -‐ 1 • log メソッドを呼び出してログメッセージをキューに入れ、そ
れを別のスレッドに処理させるクラス • シャットダウンをサポートしないプロデューサー・コンシュー
マ型のログサービス(List 7-‐13) – BlockingQueueでログ記録スレッドにメッセージを渡す
• (マルチ)プロデューサー:ログの呼び出し • (シングル)コンシューマ:ログの記録
– Takeを何度も呼んでログ記録スレッドを終わらせる • Takeはインタラプションに応答する
– 問題点 • ログに書かれていないメッセージの破棄 • logメソッドの中でブロックしていたスレッドがブロックを解かれない。
– プロデューサーとコンシューマの両方をキャンセルすることが必要
7-‐2-‐1 例:ログ記録サービス -‐ 2
• 「シャットダウンがリクエストされた」フラグでメッセージの送付を禁止(List 7-‐14) – 欠点 • 競り合い状態があるので動作が不安定 • シャットダウンの後でもメッセージをキューに入れられ
ます。 → log()でのブロッキングが発生
7-‐2-‐1 例:ログ記録サービス -‐ 3
• ログメッセージの送付をアトミックな操作にする(List 7-‐15) – 競り合い状態をなくす – シャットダウンのチェックをアトミックにして、シャッ
トダウンでなければカウンターをインクリメントし、メッセージを送付する権利を予約する
7-‐2-‐2 ExecutorServiceのシャットダウン
• 2つの終了方法は安全性と応答性のトレードオフを提供 1. Shutdown(): 穏やかなシャットダウン 2. shutdownNow(): 唐突なシャットダウン
• shutdownNowが実行中のすべてのタスクのキャンセルを試みたあと、まだスタートしていなかったタスクのリストを返す。
• 自分のスレッドの管理をExecutorServiceに委譲 – ExecutorServiceを使うログサービス(List 7-‐16) – ExecutorServiceをカプセル化するとリンクがもう一つ増え
るので、所有のつながりがアプリケーションからサービスへ、サービスからスレッドへと延びます。このつながりの各メンバが、所有するサービスやスレッドのライフサイクルを管理する
7-‐2-‐3 毒薬(Poison pill) • キューに「これをもらったら停止せよ」を意味するオブジェクトを入れておく。
– コンシューマはシャットダウンの前に自分のキューの仕事を片づけることができる
– プロデューサーはPoison pillをキューに入れた後はタスクを追加してはいけない
• クローラのインデックスの挙動 – List 7-‐17 IndexingServic: POISON Fileを定義 – List 7-‐18 IndexServiceのプロデューサスレッド: finally でpoison pillをput – List 7-‐19 IndexingServiceのコンシューマスレッド: poison pillなら breakして終
了 • Poison pill はプロデューサー・コンシューマーの数が分かっているときだ
け使える – 各プロデューサーが一つPoison pillをputし、コンシューマーはN個のPoison
pillを確認した時に終了できる
7-‐2-‐4 例:1回かぎりの実行サービス
• タスクのバッチ処理ですべてのタスクを処理するまでリターンしないメソッド
• PrivateなExecutorをつかってライフサイクルを簡単に管理できる。 – 新着メールを複数のホストの上で並列にチェック
するcheckMailメソッド (List 7-‐20) • Executorの寿命はメソッドの寿命とイコール
7-‐2-‐5 shutdownNowの制約 -‐ 1
• スタートしたけど、完了していないタスクを見つける一般的な方法はない。 – 2つの完了していない状態のタスク
1. スタートしなかったタスク 2. Executorがシャットダウンした時に進行中だったタスク
• シャットダウン時に進行中だったタスクを判断するTrackingExecutor(List 7-‐21) – ExecutorServiceをカプセル化してexecuteを書き換え
る。 – シャットダウン後にキャンセルされたタスクを記録
7-‐2-‐5 shutdownNowの制約 -‐ 2 • WebCrawler (List 7-‐22): TrackingExecutorのアプ
リ – シャットダウン時にその状態を保存して、後でリス
タートすべき。 – クローラーがシャットダウンされると、下記のタスクのurlを記録(stop メソッド) • スタートしなかったタスク • 途中でキャンセルされたタスク
– 問題点 • 完了したタスクをキャンセルと記録する競り合い状態が発
せする。 →冪等(idempotent, 2度実行しても一度実行した結果と同じ)にして対処
7-‐3 スレッドの異常終了を扱う
• アプリケーションからスレッドが漏れることを検出し、防ぐ方法
• スレッドが途中で死ぬ原因 – RunNmeExcepNon – GUIアプリのイベントディスパッチスレッド(EDT)の喪失
• フリーズする • スレッドプール内のワーカースレッドを構造化す
る例(List 7-‐23) – タスクが例外投げたらスレッドを殺す。 – 不良なタスクがその後のタスクの実行を妨げないよう
にする
7-‐3-‐1 未捕捉例外ハンドラ -‐ 1
• UncaughtExcepNonHandler – Catchされていない例外でスレッドが死んだことを
検出(Thread API) – 未捕捉の例外で終了したときJVMはイベントをUncaughtExcepNonHandler(List 7-‐24)に報告
– UncaughtExcepNonHandlerがない時、スタックとレースをSystem.errにプリント
• 未捕捉の例外はアプリのQOS次第でログに記録したりする。
7-‐3-‐1 未捕捉例外ハンドラ -‐ 2 • プールのスレッドへのUncaughtExcepNonHandlerの設
定 – ThreadPoolExecutorのコンストラクタにThreadFactoryを渡
す。 – リカバリ処理をする時
• Runnable, callableでタスクをラップ • ThreadPoolExecutorのaderExecuteフックをオーバーライト
• タスクが投げた例外が未捕捉例外ハンドラまでいくのはexecuteで依頼したタスクだけ。Submitで依頼したタスクでは、チェックされる例外もされない例外もすべて、タスクのリターンステータスの一部とみなされる。
• Submitで依頼したタスクが例外で終了すると、Future.getがExecuNonExcepNonでラップして再投する。
7-‐4 JVMのシャットダウン
1. 整然(orderly)としたシャットダウン – 最後の「正規の」スレッドが終了 – System.exit – プラットフォーム固有の終了(SIGINT, Ctr-‐C)
2. 唐突(abrupt)なシャットダウン – RunNme.halt – OSからJVMのプロセスをkill(SIGKILL)
7-‐4-‐1 シャットダウンフック -‐ 1
• 整然としたシャットダウン – Shutdown hooksを全て実行する – RunNme.addShutdownHookで登録したスレッド
• 複数のシャットダウンフックをスタートする順序は不定 • シャットダウンフック完了 → runFinalizersOnExitがtrueでファ
イナライザを実行 → 停止
• JVMの停止 – スレッドの停止、インタラプトもしないので、JVM停止
時に突然止まる – 唐突なシャットダウン。シャットダウンフックも実行され
ない。
7-‐4-‐1 シャットダウンフック -‐ 2 • シャットダウンフックはスレッドセーフであるべき。 • シャットダウンフックはサービスやアプリの後始末に使う
– 一時ファイルの削除 – OSが自動に掃除してくれない資源を掃除
• LogServiceにシャットダウンフックを登録(List 7-‐26) – ログファイルを確実にクローズ – シャットダウンフックは全員が平行に動く。
• アプリや他のシャットダウンフックがシャットダウンするかもしれないサービスに依存しないようにする。
• 全てのサービスに対応する一つのシャットダウンフックを使う。 – シャットダウンフックを使わない場合も逐次でシャットダウンアク
ションを呼び出すのは有効
7-‐4-‐2 デーモンスレッド
• 2種類のスレッド – 正規のスレッド – デーモンスレッド
• メインのスレッド以外はデーモンスレッド • 2つのスレッドの違いは終了処理 – 残っているスレッドがデーモンスレッドだけならJVMの
整然としたシャットダウン。デーモンスレッドはfinallyブロックは実行されず破棄される。
• デーモンスレッドでは、サービスのライフサイクルを正しく管理できない。
7-‐4-‐3 ファイナライザ
• ファイルやソケットのハンドルなど一部の資源あh、要らなくなったらOSに明示的に返す必要がある
• ファイナライザのあるクラスを使うことや書くことを避ける。 – 理由 • アクセスするステートの同期化が必要 • Finalizeメソッドを独自に定義しているオブジェクトの実
行性能の足を大きく引っ張る • 正しく書くのは大変難しい
まとめ
• 終末処理は、設計と実装の大きな難題の一つ。
• 強制的にキャンセルしたりスレッドを終わらせる仕組みがない。 – 協力的な介入(インタラプト)の仕組みを使う
• FutureTaskとExecutorフレームワークを使うと、キャンセルできるタスクやサービスを簡単に構築できる。
Q&A
top related