【実務・中級編】VB.NETにおけるCancellationTokenを用いた安全な非同期処理のキャンセルとタイムアウト制御 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

【VB.NET】非同期処理を支配する:CancellationTokenによる安全なキャンセルとタイムアウト制御の極意

業務自動化ツールやデスクトップアプリを開発する際、誰もが一度は直面する悪夢がある。「一度開始した重い処理(ファイル出力、API通信、DBバッチ処理)が、完了するまでフリーズして止められない」、あるいは「キャンセルボタンを押した瞬間にアプリが不正終了する」という不具合だ。

かつて.NET Frameworkの初期(あるいはVBA)の感覚を引きずった開発者は、スレッドを外部から強制終了(`Thread.Abort`など)させようとする。これは絶対にやってはならない禁忌だ。開かれたファイルハンドルは放置され、データベースの接続は破棄されず、メモリリークとデータの整合性破壊を引き起こす。

現代の.NET(.NET Coreから最新の.NET 8/9、そして.NET Framework 4.5以降)において、非同期処理を安全に中断する唯一の正道は、「協調的キャンセル(Cooperative Cancellation)」である。

今回は、VB.NETにおける`CancellationToken`および`CancellationTokenSource`を完全に掌握し、いかなる重い処理も「即座に」「安全に」「美しく」中断・制御できる、プロダクションレベルの設計パターンを伝授する。

1. なぜ「協調的キャンセル」なのか?

非同期処理(`Async / Await`)のキャンセルは、スレッドを外部から「射殺」するものではない。実行中のタスクに対して「申し訳ないが、状況が変わったので、区切りの良いところで安全に処理を切り上げてくれないか?」と『合意(協調)』を求めるプロセスである。

この合意形成を担うのが、以下の2つのオブジェクトだ。

  • `CancellationTokenSource` (CTS): キャンセル信号を「発信する」司令塔。タイムアウト時間の設定や、手動キャンセルのトリガーを引く。
  • `CancellationToken` (CT): キャンセル信号を「受信する」末端。非同期メソッドの引数として渡され、処理を中断すべきかどうかを監視する。

ライフサイクルの鉄則

`CancellationTokenSource`は、内部でOSのカーネルイベント(`WaitHandle`)やタイマーリソースを確保することがある。そのため、使い終わったら必ず`Dispose`しなければならない。 これを怠ることは、将来的なリソース枯渇(ハンドルリーク)を招く一歩となる。

2. 堅牢な設計を支える3つの黄金律

実務でバグを出さないために、設計段階で以下の3ルールを徹底せよ。

ルール①:I/Oバウンドな非同期APIには、必ずTokenを「伝搬」させる

`HttpClient.GetAsync`、`StreamWriter.WriteAsync`、`SqlCommand.ExecuteReaderAsync`など、.NET標準の非同期メソッドはほぼすべて引数に`CancellationToken`を受け取るオーバーロードを持っている。これらに対して、手元のTokenを「バケツリレー」のように最深部まで引き渡すこと。これだけで、ネットワーク遅延時のキャンセル応答性は劇的に向上する。

ルール②:CPUバウンドなループ処理には `ThrowIfCancellationRequested` を挟む

データベースから取得した数万件のデータをループで加工するような処理(CPUバウンド)では、外部の非同期APIを呼ばないため、自前でキャンセルを検知する必要がある。
ループのイテレーションごとに `token.ThrowIfCancellationRequested()` を呼び出し、キャンセルが要求されていたら即座に `OperationCanceledException` を発生させて脱出する。

ルール③:キャンセルとタイムアウトを「結合(Link)」せよ

「ユーザーがキャンセルボタンを押した時」と「接続が5秒間応答しなかった時(タイムアウト)」のどちらでも処理を中断したい場合、それぞれのTokenを `CancellationTokenSource.CreateLinkedTokenSource` で1つのTokenに結合する。これにより、呼び出し先のメソッドは、どちらの理由でキャンセルされたかを意識することなく、1つのTokenだけを監視すればよくなる。

3. 完全攻略:プロダクションコード・テンプレート

以下に、実務でそのまま使える堅牢な非同期タスク制御クラスのコードを示す。
このコードは、「ユーザーによる手動キャンセル」「10秒のシステムタイムアウト」のいずれか早い方で安全に中断し、かつロールバック処理(トランザクションのキャンセル等)を安全に行う構造を備えている。

Imports System.IO
Imports System.Net.Http
Imports System.Threading

”’

”’ 高信頼性非同期タスク実行エンジン
”’

Public Class SafeDataProcessor

‘ 疑似APIのモックURL
Private Const TargetUrl As String = “https://httpbin.org/delay/5″ ‘ 意図的に応答を遅延させるURL

”’

”’ 外部APIからデータを取得し、ファイルへ安全に書き込む非同期処理を実行します。
”’

”’ 出力先ファイルパス ”’ ユーザー操作によるキャンセル用トークン ”’ 処理成否を示すタスク
Public Async Function ExecuteProcessAsync(outputPath As String, userToken As CancellationToken) As Task(Of Boolean)

‘ 1. システムタイムアウト(10秒)用のCTSを生成
Using timeoutCts As New CancellationTokenSource(TimeSpan.FromSeconds(10))

‘ 2. ユーザーキャンセルとタイムアウトのTokenを結合(Link)する
Using linkedCts As CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(userToken, timeoutCts.Token)

Dim linkedToken As CancellationToken = linkedCts.Token
Dim client As New HttpClient()

Try
‘ A. 処理開始直前のチェック
linkedToken.ThrowIfCancellationRequested()

Console.WriteLine(“[INFO] API通信を開始します…”)

‘ B. 非同期API呼び出し。結合トークンを渡すことで、タイムアウトまたはユーザーキャンセルで即時中断される
Dim response As HttpResponseMessage = Async Await client.GetAsync(TargetUrl, linkedToken).ConfigureAwait(False)
response.EnsureSuccessStatusCode()

Dim content As String = Async Await response.Content.ReadAsStringAsync().ConfigureAwait(False)

‘ C. CPUバウンドな検証処理(データ検証を模したループ)
linkedToken.ThrowIfCancellationRequested()
Console.WriteLine(“[INFO] 取得データの検証中…”)

For i As Integer = 1 To 100
‘ 高頻度ループ内でのキャンセルチェック(パフォーマンスに配慮し、重い処理の前に挟む)
If i Mod 10 = 0 Then
linkedToken.ThrowIfCancellationRequested()
End If
Thread.Sleep(5) ‘ 擬似的な微小負荷
Next

‘ D. ファイルへの安全な非同期書き込み
Console.WriteLine(“[INFO] ファイルへ書き込んでいます…”)

‘ FileStreamをUsingで囲むことで、例外発生時(キャンセル時)も確実にロックを解放する
Using writer As New StreamWriter(outputPath, append:=False, encoding:=System.Text.Encoding.UTF8)

‘ 書き込み処理自体にもTokenを伝搬させる
Async Await writer.WriteAsync(content.AsMemory(), linkedToken).ConfigureAwait(False)
Async Await writer.FlushAsync(linkedToken).ConfigureAwait(False)

End Using

Console.WriteLine(“[SUCCESS] すべての処理が正常に完了しました。”)
Return True

Catch ex As OperationCanceledException
‘ キャンセルまたはタイムアウトが発生した場合の専用ハンドラ
‘ どのTokenが原因かを特定してログを出力する
If timeoutCts.IsCancellationRequested Then
Console.WriteLine(“[TIMEOUT] 10秒の制限時間を超過したため、処理を自動強制終了しました。”)
ElseIf userToken.IsCancellationRequested Then
Console.WriteLine(“[USER_CANCEL] ユーザーによって処理がキャンセルされました。”)
End If

‘ 【重要】ここで成果物のクリーンアップ(不完全なファイルの削除など)を行う
RollbackIncompleteFile(outputPath)
Return False

Catch ex As HttpRequestException
Console.WriteLine($”[NETWORK_ERROR] 通信エラーが発生しました: {ex.Message}”)
RollbackIncompleteFile(outputPath)
Return False

Catch ex As Exception
Console.WriteLine($”[FATAL_ERROR] 予期せぬ致命的エラー: {ex.Message}”)
RollbackIncompleteFile(outputPath)
Throw
End Try

End Using ‘ linkedCts が確実に Dispose される
End Using ‘ timeoutCts が確実に Dispose される

End Function

”’

”’ 処理が中断された際、中途半端に作成されたファイルを削除します。
”’

Private Sub RollbackIncompleteFile(filePath As String)
Try
If File.Exists(filePath) Then
File.Delete(filePath)
Console.WriteLine($”[ROLLBACK] 不完全なファイルを削除しました: {filePath}”)
End If
Catch ex As Exception
‘ ロールバック自体の失敗はログに留め、アプリをクラッシュさせない
Console.WriteLine($”[ROLLBACK_FAILED] ファイルの削除に失敗しました: {ex.Message}”)
End Try
End Sub

End Class

4. アーキテクトが語るコード解説:なぜこの書き方なのか

上記のコードには、業務アプリをクラッシュさせないための「防壁」が幾重にも張り巡らされている。

① `OperationCanceledException` のハンドリング

キャンセルが発生すると、.NETランタイムは `OperationCanceledException`(またはその派生クラス `TaskCanceledException`)をスローする。
これらを `Catch ex As Exception` で一括して一般エラーとして扱ってはならない。「キャンセルは異常系(バグや障害)ではなく、正常系(ユーザーの意思による制御)である」。そのため、専用のCatchブロックを設け、タイムアウトなのかユーザー操作なのかを識別して、適切な後処理(ロールバック)を走らせている。

② `.ConfigureAwait(False)` の徹底

UIスレッド(Windows FormsやWPF)を持つアプリケーションで非同期処理を実行する場合、呼び出し元スレッドへの復帰(同期コンテキストの復帰)はボトルネックやデッドロックの原因となる。
ライブラリやバックエンドロジック内では `.ConfigureAwait(False)` を付与し、呼び出し元のスレッドコンテキストを強制しない設計にすることで、パフォーマンスを最大化しつつデッドロックを防止する。

③ 徹底された「二重のUsing」

`CancellationTokenSource` はアンマネージドリソースを持つ。
特に `CreateLinkedTokenSource` で生成された連結TokenSourceは、親Token(`userToken`)にイベントハンドラを登録するため、Disposeを怠るとメモリリークの原因になる。`Using` 句を用いて、どのような例外が発生しようとも、スコープを抜ける際に確実に破棄されることを保証している。

5. まとめ:非同期処理の成否は「引き際」で決まる

優れた開発者と、凡庸な開発者の差は、「処理を始めること」ではなく「処理を安全に終わらせること」に対する執着の差に現れる。

非同期処理をただ `Async/Await` で繋ぐだけでは、実戦に耐えうるツールとは言えない。不慮のフリーズ、ネットワーク回線の切断、ユーザーの「やっぱり止めたい」という気まぐれに、システムがどれだけスマートに対応できるか。

今回紹介した `CancellationToken` のバケツリレーと、`LinkedTokenSource` によるタイムアウトの統合パターンを、あなたのプロジェクトの「標準設計」として組み込んでほしい。ユーザー体験の向上と、データ破損バグの撲滅を同時に達成できることを約束する。

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