【テクニカル・上級編】VB.NETでのExceptionDispatchInfoを活用したスレッド間例外の正確な再スローとエラー伝播 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

非同期の深淵:ExceptionDispatchInfoによる例外伝播の「完全なる継承」

長年、VB6のレガシーなCOMオブジェクトと格闘し、.NETの荒波を渡り歩いてきた者なら誰もが一度は直面する壁がある。それは、「別スレッドで発生した例外が、呼び出し元のメインスレッドに正しく伝播しない」という絶望的な状況だ。

多くの開発者は、`Try-Catch`でキャッチした例外を単純に`Throw`し直すか、独自のエラーオブジェクトに詰め込んで丸め込んでしまう。だが、それではスタックトレースが断ち切られ、デバッグは迷宮入りする。特に、レガシーなWindows API呼び出しが混在する複雑なシステムでは、この「情報の欠損」は命取りになる。

今日は、`.NET Framework 4.5`以降に導入された`System.Runtime.ExceptionServices.ExceptionDispatchInfo`を用いて、例外を「本来あるべき姿」で転送する、極限のハンドリング手法を伝授する。

なぜ「Throw」だけでは不十分なのか

別スレッドで発生した例外をそのまま`Throw`すると、スタックトレースは「再スローされた地点」から書き換わってしまう。これでは、真の発生源(例:ネイティブコードのアクセス違反や、非同期タスク内のロジックエラー)を見失う。

`ExceptionDispatchInfo`は、例外オブジェクトをキャプチャし、そのスタックトレースとステートを保持したまま、任意の地点で再スローさせることを可能にする。これは、非同期プログラミングにおいて「例外の真実」をメインスレッドに持ち帰るための唯一の「特権的手段」である。

実装:ExceptionDispatchInfoによる例外の完全再スロー

以下のコードは、バックグラウンドタスクで発生した例外を捕らえ、メインスレッドの`SynchronizationContext`を介さずに、元のスタックトレースを維持したまま上流へ送るためのテンプレートだ。

.net
Imports System.Runtime.ExceptionServices
Imports System.Threading.Tasks

Public Class ExceptionManager
”’

”’ バックグラウンドで発生した例外を安全にメインスレッドへ伝播させる
”’

Public Sub ExecuteTaskWithErrorHandling()
‘ 例外が発生する可能性のある非同期処理
Dim task As Task = Task.Run(Sub()
Throw New InvalidOperationException(“バックグラウンドでクリティカルな障害が発生しました。”)
End Sub)

Try
task.Wait()
Catch ae As AggregateException
‘ AggregateExceptionの内部にある実際の例外を抽出
Dim baseException As Exception = ae.Flatten().InnerException

‘ 【極限の知見】ExceptionDispatchInfoで例外をキャプチャ
‘ これにより、スタックトレースを破壊せずに再スローの準備が整う
Dim edi As ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(baseException)

‘ ここで必要に応じてログ出力やメモリのクリーンアップを行う
‘ 例: ReleaseResources()

‘ 任意のタイミングで例外を再スロー
‘ 呼び出し元は、あたかもここで例外が発生したかのように扱える
edi.Throw()
End Try
End Sub
End Class

この手法の優位性

1. スタックトレースの完全保持: `Throw ex` とは異なり、元の例外発生箇所が正確に保持される。
2. スレッド境界の超越: 異なるスレッド間で例外を安全に受け渡し、メインスレッド側の`Try-Catch`ブロックで統一的に処理できる。
3. オーバーヘッドの最小化: 重い例外オブジェクトのラップやカスタムクラスへの変換が不要。

現場で役立つ「死なないコード」のための提言

1. メモリの明示的解放(DisposeとGC)

レガシーなWindows APIを利用する場合、`ExceptionDispatchInfo`で例外をキャッチした際に、必ずハンドルやメモリの解放を行うこと。例外がスローされると`Finally`ブロックへ直行するが、COMオブジェクトや`IntPtr`で確保したメモリは、明示的に`Marshal.ReleaseComObject`や`Marshal.FreeHGlobal`を呼ばない限り、メモリリークの温床となる。

2. ログ出力の設計

例外をキャッチするたびにスタックトレースを全出力するのは非効率だ。`ExceptionDispatchInfo`で保持した例外は、一度だけ真の発生源でログを取得し、以後は型情報を確認するだけに留める。システム連携のボトルネックは、多くの場合、例外処理に伴う膨大なI/O出力にある。

3. レガシーシステムとの共存

VB6から移行したコード群は、例外をスローする代わりに`Err.Raise`や戻り値でエラーを通知する傾向がある。これらを`.NET`の例外システムに統合する際も、`ExceptionDispatchInfo`を用いて`Exception`型に変換することで、モダンなエラーハンドリング基盤に乗せることが可能だ。

アーキテクトとしての結び

プログラムとは、制御フローの芸術だ。例外を「単なるエラー」として処理するのではなく、「制御を安全に委譲するためのシグナル」として扱うことが、大規模システムを安定稼働させる秘訣である。

`ExceptionDispatchInfo`を使いこなすことは、システムの「死に様」を制御することに他ならない。貴殿が手掛けるシステムが、予期せぬ障害に直面したときこそ、この知識がスタックトレースという「遺言」を正確に届け、次の保守開発者への道標となるはずだ。

コードは嘘をつかない。書いた人間の知見の深さが、そのままシステムの耐久性に直結する。今日もまた、堅牢なコードをビルドしよう。

タイトルとURLをコピーしました