20130611 java concurrencyinpracticech7

39
Java並列処理プログラミング 第7章 キャンセルとシャットダウン 2013/6/11 遠山敏章

Transcript of 20130611 java concurrencyinpracticech7

Page 1: 20130611 java concurrencyinpracticech7

Java並列処理プログラミング  第7章  キャンセルとシャットダウン

2013/6/11  遠山敏章

Page 2: 20130611 java concurrencyinpracticech7

第7章 キャンセルとシャットダウン

•  キャンセルとインタラプションの仕組みの紹介  •  タスクやサービスをキャンセルリクエストに対

応付けるプログラムの書き方  •  インタラプションとは  – あるスレッドが別のスレッドに停止を求めることが

できる仕組み(Thread  interrupt())  –  Javaにはスレッドを途中で止める仕組みはない  

•  瞬時にスレッドを止めたいことはめったにない。  – 後始末をしてから止まるべき  

Page 3: 20130611 java concurrencyinpracticech7

目次

•  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  ファイナライザ  

Page 4: 20130611 java concurrencyinpracticech7

7-­‐1  タスクのキャンセル  -­‐  1 •  キャンセルする理由  – ユーザーがキャンセルをリクエストした  

•  GUIアプリのキャンセルボタンをクリック  – 時間制限のある活動  

•  時間内に最良の結果を返すアプリ  – アプリケーションイベント  

•  複数のタスクがそれぞれ問題空間を探索しあるタスクが解を見つけたら、他のタスクはキャンセル  

– エラー  •  クローラーのタスクのエラー  

– シャットダウン  •  穏やかなシャットダウン、緊急のシャットダウン

Page 5: 20130611 java concurrencyinpracticech7

7-­‐1  タスクのキャンセル  -­‐  2

•  スレッドを強制的に停止する安全な方法はない。  

•  協力的な仕組み  1.  「キャンセルがリクエストされた」フラグの周期的

チェック(List  7-­‐1)  1.  Cancelledをチェック。  2.  Cancelled  をvolaNleにする  

Page 6: 20130611 java concurrencyinpracticech7

7-­‐1  タスクのキャンセル  -­‐  3 •  List  7-­‐2  素数生成クラス  

–  Sleepのインタラプトが発生してもfinallyで確実にキャンセル  •  キャンセルされる側のキャンセルポリシー  

–  How,  when,  whatを定義  •  How:  キャンセルをどうやって求めるか  •  When:  キャンセルがリクエストされたことをいつチェックするか  •  What:  キャンセルのリクエストに対してタスクはどんなアクションを実

行すべきか  –  PrimeGeneratorのキャンセルポリシー  

•  How:  クライアントコードはcancelをコールしてキャンセルをリクエストする  

•  When:  PrimeGeneratorは素数が一つ見つかるたびにキャンセルをチェックする  

•  What:  キャンセルがリクエストされたことを検出したら終了する  

Page 7: 20130611 java concurrencyinpracticech7

7-­‐1-­‐1  インタラプション

•  タスクがキャンセルフラグをチェックせず永久に終わらないケース(List  7-­‐3)  – BlockingQueueのputでブロックされたスレッドはcancelledフラグをチェックできない  

•  ブロックするメソッドはインタラプションをサポート(第5章)  

•  キャンセル以外の目的にインタラプションを使うとプログラムの安定性を損ない、脆弱になる。

Page 8: 20130611 java concurrencyinpracticech7

7-­‐1-­‐1  インタラプション -­‐  2 •  Threadのインタラプション関連メソッド(List  7-­‐4)  

–  interrupt()  •  目的のスレッドをインタラプト  

–  isInterrupted()  •  目的のスレッドのインタラプテッドステータスを返す  

–  interrupted()  •  現在のスレッド(このメソッドを呼んだスレッド)のインタラプテッドステータスをクリアし、その前の値を

返す。  •  Thread  インタラプションの挙動  

–  スレッドをブロックしている時、インタラプテッドステータスをクリアし、InterruptedExcepNonを投げる  

–  スレッドがブロックしていない時、インタラプテッドステータスがセットされ、それを調べるか、調べないかはスレッドの自由。スティキーな状態。  

→ interruptメソッドは単に、インタラプションをリクエストしたというメッセージを伝えるだけ。「あなたのご都合のよろしいとき(キャンセルポイント)にお仕事を中断してください」  

•  Interruptedがtrueなら何かをすべき。  –  InterruptedExcepNonを投げる。Interruptを呼び出し、ステータスを復元

Page 9: 20130611 java concurrencyinpracticech7

7-­‐1-­‐1  インタラプション -­‐  3

•  InterruptedExcepNonを投げる。Interruptを呼び出し、ステータスを復元  

•  BrokenPrimeProducerはフラグの代わりにインタラプションを使ってキャンセルをリクエスト(List  7-­‐5)  – 2つのインタラプションのチェック  

1.  ブロックするputメソッドの中  2.  ループの明示的なポーリング  

→応答性を上げるために処理の前にインタラプションをチェックするs  

Page 10: 20130611 java concurrencyinpracticech7

7-­‐1-­‐2  インタラプションポリシー -­‐  1

•  スレッドがインタラプションリクエストをどう解釈するかという取り決め  

•  例  – インタラプションが検出されたらいつ何をするか  – インタラプションに対してはどの仕事単位をアトミック

と見なすべきか  – どんなタイミングでインタラプションに応答すべきか  

•  スレッドはインタラプションポリシーを持つべき。  •  妥当なインタラプションポリシーはスレッドレベル

又は、サービスレベルのキャンセル

Page 11: 20130611 java concurrencyinpracticech7

7-­‐1-­‐2  インタラプションポリシー -­‐  2 •  インタラプションへの反応はタスクとスレッドで違う  •  1つのインタラプトリクエストの目的が複数あることもある  

–  スレッドプールのワーカースレッドにインタラプトする  1.  現在のタスクをキャンセルせよ  2.  このワーカースレッドをシャットダウンせよ  

–  タスクは自分が所有するスレッドの中では実行されない  •  サービスからスレッドを借りる  •  スレッドを所有しないコードはインタラプテッドステータスを保全して

スレッドのオーナーがインタラプションに対応できるように注意すべき。  

•  インタラプションリクエストの検出時は、実行中の仕事を完了してインタラプションに対応すれば良い  

•  単純にInterruptedExcepNonを呼び出し側に広めるのでないならば、interruptedExcepNonをcatchしてからインタラプテッドステータスを復元すべき。:Thread.currentThread().interrupt()  

Page 12: 20130611 java concurrencyinpracticech7

7-­‐1-­‐3  インタラプションへの応答  -­‐  1

•  InterruptedExcepNonの処理  1.  例外を広める(List  7-­‐6)  2.  インタラプテッドステータスを復元して呼び出し、

スタックの上のほうがそれを処理するようにする  •  InterruptedEcepNonはもみ消してはいけない。

PrimeProducer(List  7-­‐3)でもみ消しているのはスレッドが終了するだけのため。  

Page 13: 20130611 java concurrencyinpracticech7

7-­‐1-­‐3  インタラプションへの応答  -­‐  2 •  ブロックしてインタラプ書んを受け付けるメソッド

を呼び出す活動がキャンセルをサポートしていない場合(List  7-­‐7)  → インタラプションのステータスをInterruptedExcepNonのcatch後すぐに復元せず、ステータスを保存し、リターン直前に復元すべき  –   早くセットすると無限ループになることもあるため。  

•  コードがブロックしてインタラプションを受け付けるメソッドを呼び出さない場合でも、インタラプテッドステータスをポーリングすることによって、インタラプションへの応答性を持たせられます。  

Page 14: 20130611 java concurrencyinpracticech7

7-­‐1-­‐4  例:実行時間の制限  -­‐  1 •  無限の時間のかかる問題で「最大10分だけ答えを探

せ」と指定する。  –  PrimeGenerator(List  7-­‐2)は1秒経つ前にRunNmeExcepNonを投げたら気づかれないままになる。  

•  任意のRunnableを一定時間動かす試み(List  7-­‐8)  –  ScheDuleExecutorServiceで一定時間後にキャンセル(taskThread.interrupt())を呼び出して止める  

–  例外はNedRunを呼び出す側でcatchできる。  –  この方法は反則。スレッドにインタラプションするためには

そのスレッドのインタラプションポリシーを知ってなければ、ならないため。  •  タイムアウトの前にタスクが終了したら、Returnした後で、スタート

する。

Page 15: 20130611 java concurrencyinpracticech7

7-­‐1-­‐4  例:実行時間の制限  -­‐  2 •  専用スレッドの中でタスクにインタラプトする例(List  7-­‐9)  –  2つの問題を解決  

•  aSecondPrimeOfPrimesの例外処理がcatchされなの問題  •  List  7-­‐8のキャンセルタスクの実行タイミングの問題  

–  挙動  •  NmedRunは時間制限付きのjoinをその新たに作られたスレッドで

実行。JoinでtaskThreadが終わるまで待つ  •  例外が投げられていた場合はNmedRunの呼び出したスレッドの

中で再投する  –  欠点  

•  制御が現在のスレッドに戻ったのはスレッドが正常終了したのか、joinがタイムアウトしたのか分からない。(Thread  APIの欠点)  

Page 16: 20130611 java concurrencyinpracticech7

7-­‐1-­‐5  Futureからキャンセルする

•  Futureを使って、タスクをキャンセルする(List  7-­‐10)  –  taskExec.submitでfutureを返す  –  task.cancel(false)で実行中ならインタラプトする  – タスクがキャンセルより前に例外を投げたら再投

する  

Page 17: 20130611 java concurrencyinpracticech7

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メソッド

Page 18: 20130611 java concurrencyinpracticech7

7-­‐1-­‐6  インタラプトできないブロッキングの扱い  -­‐  2

•  標準的でないキャンセルをカプセル化するReaderThread(List  7-­‐11)  –  Interruptをオーバーライドして、「標準のインタラ

プトを渡すこと」と「ソケットをクローズすること」の両方をやらせる  •  Readでブロックしていても、ブロックしてインタラプトを

受け付けるメソッドを呼び出し中でもスレッドを停止できる。

Page 19: 20130611 java concurrencyinpracticech7

7-­‐1-­‐7  標準的でないキャンセルをnewTaskForでカプセル化する  -­‐  1

•  標準的でないキャンセルをカプセル化したテクニックをThreadPoolExecutorのnewTaskForフックで洗練させる  – ExecutorServiceにCallableを依頼するとき、submit

はタスクをキャンセルするために使えるFutureを返します。  

– Future.cancel()をオーバーライドするとタスクに対して「ソケットを使うスレッドのキャンセルをカプセル化」(List  7-­‐11)するのと同等のことができる

Page 20: 20130611 java concurrencyinpracticech7

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のメソッドも呼び出せる  

Page 21: 20130611 java concurrencyinpracticech7

7-­‐2  スレッドを使っているサービスを停止する

•  シャットダウンするときはサービスが所有するスレッドも終わる必要がある。  

•  カプセル化を壊さないためにはスレッドのオーナーではないコードがスレッドを操作してはいけない。(インタラプト、プライオリティの変更)  –  スレッドプールはそのワーカースレッドのオーナーなので、

スレッドがインタラプトされたら、スレッドプールが面倒を見るべき。  

•  スレッドを所有するサービスはスレッドをシャットダウンするライフサイクルメソッドを持つべき。  –  ExecutorService    

•  shutdon(),  shutdownNow()

Page 22: 20130611 java concurrencyinpracticech7

7-­‐2-­‐1  例:ログ記録サービス  -­‐  1 •  log  メソッドを呼び出してログメッセージをキューに入れ、そ

れを別のスレッドに処理させるクラス  •  シャットダウンをサポートしないプロデューサー・コンシュー

マ型のログサービス(List  7-­‐13)  –  BlockingQueueでログ記録スレッドにメッセージを渡す  

•  (マルチ)プロデューサー:ログの呼び出し  •  (シングル)コンシューマ:ログの記録  

–  Takeを何度も呼んでログ記録スレッドを終わらせる  •  Takeはインタラプションに応答する  

–  問題点  •  ログに書かれていないメッセージの破棄  •  logメソッドの中でブロックしていたスレッドがブロックを解かれない。  

–  プロデューサーとコンシューマの両方をキャンセルすることが必要  

Page 23: 20130611 java concurrencyinpracticech7

7-­‐2-­‐1  例:ログ記録サービス  -­‐  2

•  「シャットダウンがリクエストされた」フラグでメッセージの送付を禁止(List  7-­‐14)  – 欠点  •  競り合い状態があるので動作が不安定  •  シャットダウンの後でもメッセージをキューに入れられ

ます。    →  log()でのブロッキングが発生  

Page 24: 20130611 java concurrencyinpracticech7

7-­‐2-­‐1  例:ログ記録サービス  -­‐  3

•  ログメッセージの送付をアトミックな操作にする(List  7-­‐15)  – 競り合い状態をなくす  – シャットダウンのチェックをアトミックにして、シャッ

トダウンでなければカウンターをインクリメントし、メッセージを送付する権利を予約する  

Page 25: 20130611 java concurrencyinpracticech7

7-­‐2-­‐2  ExecutorServiceのシャットダウン

•  2つの終了方法は安全性と応答性のトレードオフを提供  1.  Shutdown():  穏やかなシャットダウン  2.  shutdownNow():  唐突なシャットダウン  

•  shutdownNowが実行中のすべてのタスクのキャンセルを試みたあと、まだスタートしていなかったタスクのリストを返す。  

•  自分のスレッドの管理をExecutorServiceに委譲  –  ExecutorServiceを使うログサービス(List  7-­‐16)  –  ExecutorServiceをカプセル化するとリンクがもう一つ増え

るので、所有のつながりがアプリケーションからサービスへ、サービスからスレッドへと延びます。このつながりの各メンバが、所有するサービスやスレッドのライフサイクルを管理する

Page 26: 20130611 java concurrencyinpracticech7

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を確認した時に終了できる  

Page 27: 20130611 java concurrencyinpracticech7

7-­‐2-­‐4  例:1回かぎりの実行サービス

•  タスクのバッチ処理ですべてのタスクを処理するまでリターンしないメソッド  

•  PrivateなExecutorをつかってライフサイクルを簡単に管理できる。  – 新着メールを複数のホストの上で並列にチェック

するcheckMailメソッド  (List  7-­‐20)  •  Executorの寿命はメソッドの寿命とイコール

Page 28: 20130611 java concurrencyinpracticech7

7-­‐2-­‐5  shutdownNowの制約  -­‐  1

•  スタートしたけど、完了していないタスクを見つける一般的な方法はない。  – 2つの完了していない状態のタスク  

1.  スタートしなかったタスク  2.  Executorがシャットダウンした時に進行中だったタスク  

•  シャットダウン時に進行中だったタスクを判断するTrackingExecutor(List  7-­‐21)  –  ExecutorServiceをカプセル化してexecuteを書き換え

る。  –  シャットダウン後にキャンセルされたタスクを記録

Page 29: 20130611 java concurrencyinpracticech7

7-­‐2-­‐5  shutdownNowの制約  -­‐  2 •  WebCrawler  (List  7-­‐22):  TrackingExecutorのアプ

リ  – シャットダウン時にその状態を保存して、後でリス

タートすべき。  – クローラーがシャットダウンされると、下記のタスクのurlを記録(stop  メソッド)  •  スタートしなかったタスク  •  途中でキャンセルされたタスク  

– 問題点    •  完了したタスクをキャンセルと記録する競り合い状態が発

せする。  →冪等(idempotent,  2度実行しても一度実行した結果と同じ)にして対処

Page 30: 20130611 java concurrencyinpracticech7

7-­‐3  スレッドの異常終了を扱う

•  アプリケーションからスレッドが漏れることを検出し、防ぐ方法  

•  スレッドが途中で死ぬ原因  –  RunNmeExcepNon  – GUIアプリのイベントディスパッチスレッド(EDT)の喪失  

•  フリーズする  •  スレッドプール内のワーカースレッドを構造化す

る例(List  7-­‐23)  – タスクが例外投げたらスレッドを殺す。  – 不良なタスクがその後のタスクの実行を妨げないよう

にする

Page 31: 20130611 java concurrencyinpracticech7

7-­‐3-­‐1  未捕捉例外ハンドラ -­‐  1

•  UncaughtExcepNonHandler  – Catchされていない例外でスレッドが死んだことを

検出(Thread  API)  – 未捕捉の例外で終了したときJVMはイベントをUncaughtExcepNonHandler(List  7-­‐24)に報告  

– UncaughtExcepNonHandlerがない時、スタックとレースをSystem.errにプリント  

•  未捕捉の例外はアプリのQOS次第でログに記録したりする。

Page 32: 20130611 java concurrencyinpracticech7

7-­‐3-­‐1  未捕捉例外ハンドラ  -­‐  2 •  プールのスレッドへのUncaughtExcepNonHandlerの設

定  –  ThreadPoolExecutorのコンストラクタにThreadFactoryを渡

す。  –  リカバリ処理をする時  

•  Runnable,  callableでタスクをラップ  •  ThreadPoolExecutorのaderExecuteフックをオーバーライト  

•  タスクが投げた例外が未捕捉例外ハンドラまでいくのはexecuteで依頼したタスクだけ。Submitで依頼したタスクでは、チェックされる例外もされない例外もすべて、タスクのリターンステータスの一部とみなされる。  

•  Submitで依頼したタスクが例外で終了すると、Future.getがExecuNonExcepNonでラップして再投する。  

Page 33: 20130611 java concurrencyinpracticech7

7-­‐4  JVMのシャットダウン

1.  整然(orderly)としたシャットダウン  –  最後の「正規の」スレッドが終了  –  System.exit  –  プラットフォーム固有の終了(SIGINT,  Ctr-­‐C)  

2.  唐突(abrupt)なシャットダウン  –  RunNme.halt  –  OSからJVMのプロセスをkill(SIGKILL)

Page 34: 20130611 java concurrencyinpracticech7

7-­‐4-­‐1  シャットダウンフック  -­‐  1

•  整然としたシャットダウン  –  Shutdown  hooksを全て実行する  –  RunNme.addShutdownHookで登録したスレッド  

•  複数のシャットダウンフックをスタートする順序は不定  •  シャットダウンフック完了 →  runFinalizersOnExitがtrueでファ

イナライザを実行  →  停止  

•  JVMの停止  – スレッドの停止、インタラプトもしないので、JVM停止

時に突然止まる  – 唐突なシャットダウン。シャットダウンフックも実行され

ない。  

Page 35: 20130611 java concurrencyinpracticech7

7-­‐4-­‐1  シャットダウンフック  -­‐  2 •  シャットダウンフックはスレッドセーフであるべき。  •  シャットダウンフックはサービスやアプリの後始末に使う  

–  一時ファイルの削除  –  OSが自動に掃除してくれない資源を掃除  

•  LogServiceにシャットダウンフックを登録(List  7-­‐26)  –  ログファイルを確実にクローズ  –  シャットダウンフックは全員が平行に動く。  

•  アプリや他のシャットダウンフックがシャットダウンするかもしれないサービスに依存しないようにする。  

•  全てのサービスに対応する一つのシャットダウンフックを使う。  –  シャットダウンフックを使わない場合も逐次でシャットダウンアク

ションを呼び出すのは有効  

Page 36: 20130611 java concurrencyinpracticech7

7-­‐4-­‐2  デーモンスレッド

•  2種類のスレッド  – 正規のスレッド  – デーモンスレッド  

•  メインのスレッド以外はデーモンスレッド  •  2つのスレッドの違いは終了処理  – 残っているスレッドがデーモンスレッドだけならJVMの

整然としたシャットダウン。デーモンスレッドはfinallyブロックは実行されず破棄される。  

•  デーモンスレッドでは、サービスのライフサイクルを正しく管理できない。  

Page 37: 20130611 java concurrencyinpracticech7

7-­‐4-­‐3  ファイナライザ

•  ファイルやソケットのハンドルなど一部の資源あh、要らなくなったらOSに明示的に返す必要がある  

•  ファイナライザのあるクラスを使うことや書くことを避ける。  – 理由  •  アクセスするステートの同期化が必要  •  Finalizeメソッドを独自に定義しているオブジェクトの実

行性能の足を大きく引っ張る  •  正しく書くのは大変難しい  

Page 38: 20130611 java concurrencyinpracticech7

まとめ

•  終末処理は、設計と実装の大きな難題の一つ。  

•  強制的にキャンセルしたりスレッドを終わらせる仕組みがない。  – 協力的な介入(インタラプト)の仕組みを使う  

•  FutureTaskとExecutorフレームワークを使うと、キャンセルできるタスクやサービスを簡単に構築できる。  

Page 39: 20130611 java concurrencyinpracticech7

Q&A