【テクニカル・上級編】ProgressBarとTask.Runを組み合わせたモダンな非同期進捗表示:キャンセルトークンによる安全な処理中断の実装 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

【Windows Forms極限設計】ProgressBarとTask.Runによる非同期進捗表示:キャンセルトークン完全掌握のレシピ

レガシーな業務システムにおいて、「ボタンを押した瞬間に画面がフリーズし、Windowsから『応答なし』と宣告される」という悪夢は、いまだに現場のエンジニアを苦しめている。
原因は単純だ。UIスレッド(Main Thread)上で数分を要するループ処理やデータベースI/Oを直列実行しているからに他ならない。

VBAからVB.NETへ移行した世代の多くが陥る罠が、「`Application.DoEvents`の乱用」だ。あれは非同期処理ではなく、単なるイベントループのネストであり、スタックオーバーフローや予期せぬ再入可能性(Reentrancy)の温床となる。

今回は、Windows Formsアーキテクチャの根幹を理解し、`Task.Run`、`IProgress(Of T)`、そして`CancellationToken`を組み合わせた、モダンかつ極限まで安全な非同期進捗表示と処理中断パターンを解説する。

—

1. アーキテクチャの基本方針:なぜTask.RunとIProgressなのか?

WinFormsのUIコントロール(`ProgressBar`や`Label`など)は、作成されたスレッド(通常はUIスレッド)以外のスレッドから直接操作してはならない。これはスレッドセーフではなく、`.NET`ランタイムにおいてクロススレッド例外を引き起こすか、最悪の場合メモリ破壊を招く。

これを解決するのが以下の2つの柱だ。

1. `Task.Run`によるバックグラウンド処理
CPUバウンド、あるいはI/Oバウンドな重い処理を、ThreadPoolのスレッドへ完全にオフロードし、UIスレッドを解放する。
2. `IProgress(Of T)`を通したスレッドセーフな報告
`BackgroundWorker`(レガシーなコンポーネント)のイベント駆動モデルは捨て去る。`IProgress(Of T)`は、内部で自動的に`SynchronizationContext`をキャプチャし、UIスレッドへ安全にコールバックをマーシャリングする。

—

2. 実装コード:完全自己完結型フォーム

以下のコードは、Visual StudioのWindows Formsプロジェクトにそのまま貼り付けて動作確認できる、極限まで最適化された実装である。

Imports System.Threading
Imports System.Threading.Tasks

Public Class MainForm
Inherits Form

Private WithEvents btnStart As Button
Private WithEvents btnCancel As Button
Private progressBar As ProgressBar
Private lblStatus As Label

‘ キャンセルトークンを発行するためのソース
Private _cts As CancellationTokenSource

Public Sub New()
‘ UIコントロールの初期化(デザイナーコードの代用)
Me.Text = “極限非同期プログレス & キャンセル制御”
Me.Width = 500
Me.Height = 250
Me.StartPosition = FormStartPosition.CenterScreen

btnStart = New Button() With {.Text = “処理開始”, .Left = 30, .Top = 30, .Width = 120, .Height = 40}
btnCancel = New Button() With {.Text = “処理中断”, .Left = 160, .Top = 30, .Width = 120, .Height = 40, .Enabled = False}

progressBar = New ProgressBar() With {.Left = 30, .Top = 90, .Width = 420, .Height = 30, .Minimum = 0, .Maximum = 100}
lblStatus = New Label() With {.Left = 30, .Top = 140, .Width = 420, .Height = 25, .Text = “待機中…”}

Me.Controls.AddRange(New Control() {btnStart, btnCancel, progressBar, lblStatus})
End Sub

‘ —————————————————-
‘ 処理開始ボタンクリック(Async/Await パターン)
‘ —————————————————-
Private Async Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
‘ UIの状態切替
btnStart.Enabled = False
btnCancel.Enabled = True
progressBar.Value = 0

‘ CancellationTokenSourceのインスタンス化
_cts = New CancellationTokenSource()

‘ IProgressの実装:進捗状況とステータス文字列を同時に受け取る(Tupleを使用)
Dim progress = New Progress(Of (Percent As Integer, Message As String))(
Sub(data)
progressBar.Value = data.Percent
lblStatus.Text = data.Message
End Sub
)

Try
‘ Task.Runでバックグラウンドスレッドへ処理を完全委譲
‘ フォームの破棄や例外をハンドリングするためAwaitする
Await Task.Run(
Sub()
ExecuteHeavyBatchProcess(_cts.Token, progress)
End Sub,
_cts.Token
)

lblStatus.Text = “処理が正常に完了しました。”

Catch ex As OperationCanceledException
‘ キャンセル時の例外処理(異常系ではないためログやステータス表示のみ)
lblStatus.Text = “ユーザーによって処理が中断されました。”
progressBar.Value = 0

Catch ex As Exception
‘ 予期せぬ例外の捕捉
MessageBox.Show($”予期せぬエラーが発生しました: {ex.Message}”, “エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
lblStatus.Text = “エラーにより中断しました。”

Finally
‘ リソースの確実な解放とUIの復元
If _cts IsNot Nothing Then
_cts.Dispose()
_cts = Nothing
End If

btnStart.Enabled = True
btnCancel.Enabled = False
End Try
End Sub

‘ —————————————————-
‘ 処理中断ボタンクリック
‘ —————————————————-
Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click
If _cts IsNot Nothing Then
‘ キャンセル要求を発行(スレッドセーフ)
_cts.Cancel()
btnCancel.Enabled = False
lblStatus.Text = “中断要求を送信中…”
End If
End Sub

‘ —————————————————-
‘ バックグラウンドで実行される重いバッチ処理
‘ —————————————————-
Private Sub ExecuteHeavyBatchProcess(token As CancellationToken, progress As IProgress(Of (Percent As Integer, Message As String)))
Const totalSteps As Integer = 100

For i As Integer = 1 To totalSteps
‘ 1. ループの先頭でキャンセル要求が来ていないかチェックする
‘ トークンがキャンセルされている場合、OperationCanceledExceptionがスローされる
token.ThrowIfCancellationRequested()

‘ 擬似的な重い処理(CPUバウンド / I/Oバウンド)
Thread.Sleep(50)

‘ 2. 進捗状況を報告(UIスレッドへマーシャリングされる)
Dim currentPercent As Integer = CInt((i / totalSteps) 100)
progress.Report((currentPercent, $”処理中… ステップ {i} / {totalSteps}”))
Next
End Sub

‘ —————————————————-
‘ フォーム破棄時のメモリリーク防止(重要)
‘ —————————————————-
Protected Overrides Sub OnFormClosed(e As FormClosedEventArgs)
If _cts IsNot Nothing Then
_cts.Cancel()
_cts.Dispose()
_cts = Nothing
End If
MyBase.OnFormClosed(e)
End Sub

End Class

—

ジニアが注目すべき3つの技術的要所

1. `CancellationToken` と `ThrowIfCancellationRequested()` の協調動作

キャンセル処理の本質は、「スレッドを強制終了(Kill)することではない」。強制終了はデッドロックやリソースの解放漏れ(ファイルハンドルやDB接続の解放忘れ)を引き起こす最悪のアンチパターンである。
バックグラウンドスレッド側が自主的にキャンセル要求を検知し、安全に処理を巻き戻して終了(Graceful Shutdown)するための合図が`CancellationToken`である。ループの要所や、重いDBクエリの実行前に `token.ThrowIfCancellationRequested()` を挟むことで、極めて安全な中断を実現している。

2. 複数の値を同時に渡す `IProgress(Of T)` の型設計

進捗率(%)だけでなく、現在の処理詳細(テキスト)も同時にUIへ伝えたい場合がある。VB.NETでは、タプル(Tuple)構文 `IProgress(Of (Percent As Integer, Message As String))` を利用することで、クラスをわざわざ定義するコストを支払うことなく、構造化されたデータを美しくスレッドセーフに受け渡すことができる。

3. `CancellationTokenSource` のライフサイクル管理

`CancellationTokenSource`(`_cts`)は、内部でタイマーや同期プリミティブを保持している場合があるため、使い捨て(Dispose必須)のオブジェクトである。
`Try…Catch…Finally` ブロックの `Finally` で確実に `.Dispose()` を呼び出すこと。また、ユーザーが処理中にフォームを閉じた場合を想定し、`OnFormClosed` オーバーライド内でも確実にキャンセルと破棄を行うのが、堅牢なシステム設計のプロフェッショナルとしての最低限の責務である。

—

結び:レガシーの呪縛を断ち切れ

VBAの `DoEvents` や、同期ブロックの泥臭いハックにしがみついているうちは、モダンなWindowsデスクトップアプリケーションのパフォーマンスと信頼性を語る資格はない。
`Async` / `Await`、`Task.Run`、そして `CancellationToken`。この三種の神器を正しく使いこなすことで、コードは劇的にクリーンになり、ユーザーは「フリーズしない快適な業務システム」を手に入れる。

真のエンジニアリングとは、動くものを作ることではなく、「予期せぬ異常系や中断要求に直面しても、決して崩壊しない堅牢な構造」をコードに宿すことだ。今日の設計を、明日からの現場のスタンダードにしてほしい。

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