非同期処理の「迷宮」から脱出せよ:Async/AwaitとIProgress(Of T)で構築する堅牢なUIフィードバック
現場で業務自動化ツールを開発していると、必ずぶつかる壁がある。
「重い処理を実行中、画面がフリーズして応答なしになる」。
この現象を回避するために、安易に `DoEvents` を叩いたり、スレッドを強引に操作してアプリをクラッシュさせるエンジニアを私は何度も見てきた。
VB.NETにおける非同期処理は、もはや「オプション」ではない。UIのレスポンスを維持し、かつ処理の進捗を正確にユーザーに伝えるための「必須の作法」だ。今回は、`Async/Await` と `IProgress(Of T)` を組み合わせ、保守性が高く、かつ実務でそのまま使える堅牢な設計を伝授する。
—
1. なぜ「非同期」で設計するのか
多くのVB.NET開発者が犯す最大のミスは、「重い処理の中にUIの更新ロジックを直書きすること」だ。
これには2つの大きなリスクがある。
- 密結合の弊害: 処理ロジックとUI表示が混ざり合い、単体テストが困難になる。
- スレッド安全性の欠如: 別スレッドからUIコントロールを直接操作しようとして `InvalidOperationException`(クロススレッド操作の例外)が発生する。
これらを解決する唯一無二の正解が、「ロジックは非同期(Async)で独立させ、進捗報告だけをIProgressインターフェース経由でUIに委譲する」という設計パターンだ。
—
2. 実装の核心:IProgress(Of T)の役割
`IProgress(Of T)` は、バックグラウンド処理からUIスレッドへ「安全に」進捗情報を投げ込むための通信路だ。これを使うことで、開発者はスレッドの同期を意識する必要がなくなる。`.NET` 側が裏側で自動的にUIスレッドへのマーシャリングを行ってくれるからだ。
プロダクションコード例
以下のコードは、ファイル処理などの重いタスクを想定した設計だ。これをそのままクラスライブラリやフォームのメソッドとして利用してほしい。
Imports System.Threading
Imports System.Threading.Tasks
Public Class ProgressManager
‘ 重い処理をシミュレートする非同期メソッド
‘ UIに依存しない設計にすることが鉄則
Public Async Function ExecuteHeavyTaskAsync(progress As IProgress(Of Integer), ct As CancellationToken) As Task
Dim totalSteps As Integer = 100
For i As Integer = 1 To totalSteps
‘ キャンセル要求のチェック
ct.ThrowIfCancellationRequested()
‘ ここで本来の重い業務処理(DB更新やファイルI/Oなど)を行う
Await Task.Delay(50)
‘ 進捗を報告
Dim percentage As Integer = CInt((i / totalSteps) 100)
progress?.Report(percentage)
Next
End Function
End Class
—
3. UI側(Form)での呼び出し方
呼び出し側のUIコードは、極めてシンプルになる。`Progress(Of T)` を生成し、そこに「進捗があったらどうするか」というデリゲートを渡すだけだ。
Public Class MainForm
Private cts As CancellationTokenSource
Private Async Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
‘ 連続クリック防止
btnStart.Enabled = False
progressBar.Value = 0
cts = New CancellationTokenSource()
‘ 進捗報告用のインスタンスを作成
‘ このラムダ式は自動的にUIスレッドで実行される
Dim progressReporter As New Progress(Of Integer)(Sub(percent)
progressBar.Value = percent
End Sub)
Try
Dim manager As New ProgressManager()
Await manager.ExecuteHeavyTaskAsync(progressReporter, cts.Token)
MessageBox.Show(“処理が完了しました。”)
Catch ex As OperationCanceledException
MessageBox.Show(“処理がキャンセルされました。”)
Catch ex As Exception
MessageBox.Show($”エラー発生: {ex.Message}”)
Finally
btnStart.Enabled = True
cts.Dispose()
End Try
End Sub
Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click
cts?.Cancel() ‘ キャンセル要求を送信
End Sub
End Class
—
4. 現場で生き残るための「鉄則」
このコードをプロダクション環境に投入する際、以下の3点を必ず守ってほしい。
1. CancellationTokenを活用せよ: ユーザーは「待てない」生き物だ。キャンセルボタンを用意し、`CancellationToken` で確実にタスクを停止できるようにせよ。これを実装しないツールは、いずれメモリリークや無駄なDB接続を招く。
2. 例外の集約: `Async` メソッド内で発生した例外は、`Await` を呼び出した側(UI側)で補足する必要がある。`Try-Catch` を忘れると、アプリは静かに、かつ確実に死ぬ。
3. UIのロックを忘れるな: 処理中はボタンを `Enabled = False` にする。非同期処理の多重起動は、データの整合性を破壊する最大の原因だ。
まとめ:エンジニアの誇りとして
「動けばいい」というコードは、数ヶ月後の自分を苦しめる負債でしかない。
`Async/Await` と `IProgress(Of T)` を正しく使うことは、単に画面を固まらせないためだけではなく、「処理の責任範囲を明確にし、UIとビジネスロジックを分離する」というオブジェクト指向の基本原則を実践することに他ならない。
この設計をマスターすれば、あなたの作るツールは「単なる作業用スクリプト」から、現場で信頼される「業務アプリケーション」へと昇華するはずだ。さあ、今すぐ既存のコードをリファクタリングしてみよう。
