2016/01/11
関数内でのリソース解放処理
注:最後に書きましたが、この記事の内容には未解決問題が残っています。
はじめに
前回は、パイプラインを下流の関数内で打ち切る方法について説明しました。
今回は逆に、下流でパイプラインが打ち切られた場合、上流の関数ではどう対応すべきなのか、特にリソース解放処理を焦点にして考察してみます。
パイプラインが打ち切られるケース
前回も説明しましたが、パイプライン内の関数またはコマンドレットで何らかの例外が発生すると、パイプライン処理がその時点で打ち切られます。パイプライン処理が打ち切られると、後続のprocessブロックのみならず、パイプラインに含まれるすべてのendブロックの実行もスキップされてしまいます。
ところでコマンドレットや関数では何かエラーが発生した場合、コマンドレットではWriteErrorメソッド、関数ではWrite-Errorコマンドレットを使ってエラーストリームに、生の例外オブジェクトをラップしたErrorRecordを出力し、呼び出し側のErrorActionの設定にエラー時の処理を委ねるのが基本です。
呼び出し時のErrorAction指定がContinue、SilentlyContinue、Ignoreの場合は、パイプラインが中断することはありませんが、StopやInquireで中断した場合は例外(ActionPreferenceStopException)がthrowされ、パイプラインは打ち切られます。
(ちなみにv3からはStopで中断した場合は、コマンドレットがエラーストリームに書き出したErrorRecordに含まれるExceptionがthrowされる)
また、継続不能エラーが発生(ThrowTerminatingErrorメソッド)したり、.NETの生の例外がそのままthrowされたり(お行儀が悪いですが)、breakステートメント(FlowControlException)が実行されたり、Select-Object -First(StopUpstreamCommandsException)が実行された場合も、同様にパイプラインは打ち切られます。
つまり、下流のコマンドでパイプラインが打ち切られるケースというのは色々あり得るので、endブロックというのは実行が確約されたものでは全くない、ということに留意しておく必要があります。
関数内のリソース解放処理
確実に実行したい後処理というのは色々あると思いますが、特に確保したリソースの解放というのは確実に実行してもらわないと困ります。
しかし、前述のような背景があるので、何らかのリソースを用いる関数を書く際に、beginブロックでリソースを確保し、processブロックでリソースを利用し、endブロックでリソースを解放する、ということは基本はNGということになります。
この点、コマンドレットクラスであれば、IDisposableインターフェースを実装しておけば、コマンドレット終了時にDisposeメソッドをPowerShellが呼んでくれるので、その中にリソース解放処理を記述しておけばOKです。しかし関数ではこの手法が使えないので、代替案を考える必要があります。
パイプライン処理の後始末をしよう - 鷲ノ巣であえとす氏が考案した、beginブロックに後処理用の関数を定義しておく方法では、同じ関数内のprocessブロックで発生したエラーをcatchしてリソース解放処理を走らせることは可能です。が、残念ながら下流で発生した例外をcatchすることはできず、その場合は後処理がスキップされてしまいます。
リソース解放処理を含めた関数
あえとす氏の方法を若干アレンジして、下流で例外が発生した場合も確実にリソース解放の後処理を走らせる方法を考えてみました。以下にコードを示します。
※クラス構文を使ってますが、これは単に、Disposeできるオブジェクトのサンプルだと思ってください。
class SomeResource : IDisposable { [void]Dispose() { # ここで何らかのリソース解放処理を行ったものとする } } function Write-OutputWithResource { [CmdletBinding()] param([Parameter(ValueFromPipeline)][psobject[]]$InputObject) begin { $resource = New-Object SomeResource # 後で解放する必要のある何らかのリソース確保 function Clear-Resource { # リソースの解放処理 $resource.Dispose() } } process { try { $processing = $true Write-Verbose "output_start: $InputObject" $InputObject # パイプライン出力 Write-Verbose "output_end: $InputObject" $processing = $false } catch { # try内で例外が発生した場合はそのまま再スロー throw } finally { if($processing) { Write-Verbose "resource_dispose (on error)" Clear-Resource } Write-Verbose "output_finally: $InputObject" } } end { Write-Verbose "resource_dispose (at end)" Clear-Resource } }
processブロック内のtryブロックでパイプライン下流に値を出力したとき、下流で例外が出てもcatchブロックが実行されないので、代わりに、必ず実行されるfinallyブロックから後処理を呼び出すようにしてみました。
ただし、パイプライン下流で例外が発生しなかった場合には、processブロック内ではリソース解放処理はしたくないので、例外発生の有無を$processingという変数の値を見ることで確認しています。もしパイプライン出力したあと下流で例外が出ていれば、$processingの値は$trueのままになるので判断可能です。
パイプラインが中断することなく、最後まで実行される場合は、endブロック内でリソース解放処理を行います。
同じ関数内で例外が発生したときのリソース解放処理についても、$processing = $trueと$processing = $falseの間に例外が発生する可能性のある処理を記述した上で、catchブロックで再throwすれば、併せて対応できるのではないかと思います。(それ用のtry..catchを記述してもいいですが)
関数の実行例
PS> 1..3 | Write-OutputWithResource -Verbose | Select-Object -First 2 詳細: output_start: 1 1 詳細: output_end: 1 詳細: output_finally: 1 詳細: output_start: 2 2 詳細: resource_dispose (on error) 詳細: output_finally: 2
このように、下流でパイプライン打ち切りがあるとendブロックは実行されませんが、リソース解放処理は、きちんとprocessブロック内のfinallyブロックから呼び出されています。
PS> 1..3 | Write-OutputWithResource -Verbose 詳細: output_start: 1 1 詳細: output_end: 1 詳細: output_finally: 1 詳細: output_start: 2 2 詳細: output_end: 2 詳細: output_finally: 2 詳細: output_start: 3 3 詳細: output_end: 3 詳細: output_finally: 3 詳細: resource_dispose (at end)
もちろんパイプラインが最後まで正常に実行された場合も、ちゃんと最後にendブロックでリソース解放が行えるようになっています。
おわりに
本当に、こうするしかないんですかね…?
PowerShellにもリソース利用のusingステートメント欲しいです、が、begin, process, endにまたがって機能するusingってどういう構文になるんでしょうね。
(14:22追記)と、ここまで書いておいて、この方法では下流で発生した例外には対処できますが、上流で例外が発生した場合はリソース解放が実行されないという問題に気付きました…。どうすればいいんだ…。
プライバシーポリシー