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

スポンサーリンク

Windows Formsの限界を超える:Task.RunとCancellationTokenで実現する「絶対にフリーズしない」非同期プログレスバー設計

業務アプリケーションの現場において、「重いバッチ処理を実行した瞬間、画面が真っ白になり『応答なし』と表示される」という悪夢は、いまだに絶えない。ユーザーは不安に駆られ、強制終了ボタンを押し、データベースには中途半端なトランザクションが残る。

なぜ、いまだにこのようなレガシーな実装が蔓延しているのか?
それは多くのプログラマが、UIスレッド(Main Thread)とバックグラウンド処理の境界線を曖昧にしたまま、場当たり的なコードを書いているからに他ならない。

今回は、Visual Basic .NET(VB.NET)のWindows Forms環境において、`Task.Run`、`IProgress(Of T)`、そして`CancellationToken`を完全に調停させ、「滑らかな進捗表示」と「安全な処理中断」を両立するプロダクションコードの設計思想を伝授する。

—

1. なぜ「DoEvents」地獄は悪なのか?

古いVBのサンプルコードを見ると、長時間のループ処理の中に `Application.DoEvents()` をねじ込んでいるコードによく遭遇する。

‘ 【アンチパターン】絶対にやってはいけない実装
For i As Integer = 1 To 10000
‘ 重い処理
Thread.Sleep(10)
Application.DoEvents() ‘ UIの固まりを防いでいる「つもり」になる魔薬
Next

このアプローチは百害あって一利なしだ。
`DoEvents` はメッセージキューを強制的に割り込み処理させるため、リエントラント(再入可能性)問題を引き起こす。ユーザーが誤って再度ボタンを押した際、同じ処理が多重起動し、データベースの競合やメモリリークの温床となる。

我々が目指すべきは、UIスレッドを完全に解放しつつ、安全にバックグラウンドタスクをコントロールする真の非同期アーキテクチャである。

—

2. 堅牢な非同期設計の3大要素

VB.NETでモダンな非同期処理を構築するには、以下の3つのコンポーネントを正確に咬合させる必要がある。

1. `Task.Run`: CPUバウンドな重い処理を、ThreadPoolのスレッドへ安全にオフロードする。
2. `IProgress(Of T)`: スレッドセーフにUIへ進捗状況を報告する。クロススレッド例外を意識する必要をなくす現代の必須インターフェース。
3. `CancellationTokenSource` / `CancellationToken`: 協調的キャンセル(Cooperative Cancellation)を実現し、安全にタスクを停止させる。

—

3. 実装:コピペで使えるプロダクションコード

以下のコードは、Windows Forms上に `ProgressBar` 1つ、`Button`(開始用)1つ、`Button`(キャンセル用)1つを配置したフォームを想定した、そのまま実務で使える完全な実装である。

Imports System.Threading
Imports System.Threading.Tasks

Public Class Form1
‘ キャンセルリクエストを管理するトークンソース
Private _cts As CancellationTokenSource

Private Async Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
‘ UIの状態制御:開始時はボタンを切り替え、プログレスバーを初期化
btnStart.Enabled = False
btnCancel.Enabled = True
ProgressBar1.Value = 0
ProgressBar1.Maximum = 100

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

‘ 進捗報告を受け取るためのIProgressの実装
‘ UIスレッド上で安全にProgressBarの値を更新する
Dim progress As IProgress(Of Integer) = New Progress(Of Integer)(Sub(percent)
ProgressBar1.Value = percent
End Sub)

Try
‘ Task.Runで重い処理をバックグラウンドスレッドへ逃がす
‘ Awaitを使用することで、UIスレッドをブロックせずに処理の完了を待機する
Await Task.Run(Sub()
ExecuteHeavyBatchProcess(progress, _cts.Token)
End Sub, _cts.Token)

MessageBox.Show(“バッチ処理が正常に完了しました。”, “完了”, MessageBoxButtons.OK, MessageBoxIcon.Information)

Catch ex As OperationCanceledException
‘ キャンセルされた場合の例外捕捉
MessageBox.Show(“処理はユーザーによって安全に中断されました。”, “中断”, MessageBoxButtons.OK, MessageBoxIcon.Warning)

Catch ex As Exception
‘ その他の予期せぬ例外
MessageBox.Show($”エラーが発生しました: {ex.Message}”, “エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)

Finally
‘ 資源の解放とUIの復元
If _cts IsNot Nothing Then
_cts.Dispose()
_cts = Nothing
End If

btnStart.Enabled = True
btnCancel.Enabled = False
ProgressBar1.Value = 0
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 ‘ 連打防止
End If
End Sub

”’

”’ バックグラウンドで実行される重いバッチ処理(DB連携やファイル処理を想定)
”’

Private Sub ExecuteHeavyBatchProcess(progress As IProgress(Of Integer), token As CancellationToken)
Const totalSteps As Integer = 50

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

‘ — ここに実際の重い処理(DB書き込み、API呼び出し、ファイルIOなど)を記述 —
Thread.Sleep(100) ‘ 擬似的な負荷(100ms × 50回 = 5秒)
‘ ————————————————————————

‘ 2. 進捗率を計算してUIへ報告
Dim currentPercent As Integer = CInt((i / CSng(totalSteps)) 100)
If progress IsNot Nothing Then
progress.Report(currentPercent)
End If
Next
End Sub
End Class

—

4. チーフアーキテクトが解説する実装の急所

A. なぜ `IProgress(Of T)` なのか?

従来のVB.NETでは、別スレッドからUIを操作しようとすると `InvalidOperationException`(クロススレッド操作のエラー)が発生するため、`Invoke` や `BeginInvoke` を書く必要があった。
しかし、`Progress(Of T)` を使うと、インスタンスが生成されたスレッド(ここではUIスレッド)のコンテキストを自動的にキャプチャし、安全にUIコントロールのプロパティを書き換えてくれる。コードの可読性が劇的に向上する理由がここにある。

B. 協調的キャンセル(Cooperative Cancellation)の厳格さ

`_cts.Cancel()` を呼び出しても、OSが強制的にバックグラウンドスレッドを殺すわけではない。
スレッド側が `token.ThrowIfCancellationRequested()` を定期的に呼び出すことで初めて、「あ、キャンセルされたんだな」と気づき、自発的に処理を中断(`OperationCanceledException` をスロー)する。
「ファイル書き込みのトランザクション途中でキャンセルされないよう、安全な区切り(トランザクション境界)でのみトークンチェックを行う」といった設計上のコントロールが、データ破損を防ぐ鍵となる。

C. 例外ハンドリングの順序

`Catch ex As OperationCanceledException` は、一般的な `Catch ex As Exception` よりも手前に記述しなければならない。なぜなら、キャンセル例外も `Exception` の派生クラスだからだ。この順序を間違えると、意図したキャンセル処理が「予期せぬエラー」として扱われてしまうため注意が必要である。

—

5. おわりに:プロフェッショナルなコードを書くということ

動けばいいだけのコードは、素人でも書ける。
しかし、ユーザーがシステムを信頼し、快適に業務を遂行できる「堅牢なUI」を担保するのは、設計思想を理解したプロフェッショナルエンジニアの仕事だ。

今回紹介した `Task.Run` × `IProgress` × `CancellationToken` のパターンは、Windows Formsにおける非同期処理の「ゴールドスタンダード」である。ぜひ自身のプロジェクトへ導入し、フリーズとは無縁の洗練されたシステムを実現してほしい。

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