BackgroundWorkerを使いこなせ:レガシーコードを「堅牢な非同期処理」へ変貌させる極意
VB.NETの現場で、今なお避けて通れない存在が`BackgroundWorker`だ。最新の`async/await`パターンに置き換えるべきという議論はもっともだが、既存の巨大なWinFormsアプリケーションを全リファクタリングするのは現実的ではない。
重要なのは「レガシーだから不安定」という言い訳を捨て、「BackgroundWorkerの制約を完全に掌握し、安全に実装する」ことだ。今回は、多くのエンジニアが陥る「UIフリーズ」と「不正なスレッド操作」を根絶し、プロダクション環境で耐えうる設計を伝授する。
—
1. なぜ「中途半端な実装」は失敗するのか
`BackgroundWorker`で最も多い失敗は、「DoWorkイベント内で直接UIコントロールを操作すること」と「キャンセル処理の投げっぱなし」だ。
- UIスレッド違反: `DoWork`はバックグラウンドスレッドで動く。ここで`TextBox.Text = “完了”`などと書けば、いずれ「クロススレッド操作の例外」が発生する。
- キャンセル漏れ: `CancelAsync()`を呼んでも、処理側が定期的に`CancellationPending`をチェックしていなければ、処理は止まらない。
これらを防ぐには、「状態の分離」と「コールバックのルール化」を徹底する必要がある。
—
2. 堅牢な実装のためのデザインパターン
以下のコードは、ファイル処理やDBアクセスを想定した、安全かつ保守性の高いテンプレートだ。
‘ フォームクラス内に実装するBackgroundWorkerの標準的な構成
Public Class MainForm
‘ 処理開始時のボタン押下イベント
Private Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
‘ 二重起動防止
btnStart.Enabled = False
btnCancel.Enabled = True
progressBar.Value = 0
‘ 非同期処理の開始
BackgroundWorker1.RunWorkerAsync()
End Sub
‘ 【重要】バックグラウンドで行う処理本体
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim worker As System.ComponentModel.BackgroundWorker = DirectCast(sender, System.ComponentModel.BackgroundWorker)
For i As Integer = 1 To 100
‘ 1. キャンセル要求の監視(これが無いと止まらない)
If worker.CancellationPending Then
e.Cancel = True
Exit For
End If
‘ 2. ここにDB更新やファイルI/O等の重い処理を記述
System.Threading.Thread.Sleep(50) ‘ 負荷をシミュレーション
‘ 3. 進捗報告(UIスレッドへ安全に値を渡す)
worker.ReportProgress(i, $”処理中… {i}%”)
Next
End Sub
‘ UIスレッドで実行される進捗更新
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
progressBar.Value = e.ProgressPercentage
lblStatus.Text = e.UserState.ToString() ‘ ReportProgressから渡された文字列
End Sub
‘ 終了処理(正常・キャンセル・エラーすべてここを通る)
Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
If e.Cancelled Then
lblStatus.Text = “処理がキャンセルされました。”
ElseIf e.Error IsNot Nothing Then
MessageBox.Show(“エラー発生: ” & e.Error.Message)
Else
lblStatus.Text = “完了しました。”
End If
btnStart.Enabled = True
btnCancel.Enabled = False
End Sub
‘ キャンセルボタン
Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click
BackgroundWorker1.CancelAsync()
End Sub
End Class
—
3. 実務で「事故らない」ための3つの鉄則
① ループ処理の「刻み」を意識する
ファイル読み込みやDBクエリなど、一回の処理が数秒かかる場合、`CancellationPending`のチェックが機能しない。
`For`ループや`While`ループの中に必ずチェックを入れろ。もし巨大なデータを一行ずつ処理するなら、100行ごとにチェックするといった「粒度」の調整が必要だ。
② ReportProgressを乱用しない
`ReportProgress`はUIスレッドへメッセージを送るため、極端に頻度が高い(ミリ秒単位など)とUIが逆に重くなる。進捗は「1%単位」や「特定のイベント発生時」に絞るのが、パフォーマンス維持の定石だ。
③ 例外処理(Error Handling)の集中管理
`DoWork`内で発生した例外は、そのままではアプリケーションをクラッシュさせる。必ず`Try…Catch`で囲み、`RunWorkerCompleted`の`e.Error`プロパティを通じてUI側へ通知する設計にせよ。スレッド内で無理やり`MessageBox`を出そうとするのは、デバッグを困難にする悪手である。
—
最後に:なぜこの設計なのか
エンジニアがコードを書く目的は、ただ動かすことではない。「後から見た自分が、何も考えずにメンテナンスできること」だ。
`BackgroundWorker`はレガシーかもしれないが、上記の通り「キャンセル監視」「進捗報告」「完了通知」というサイクルを徹底すれば、現代のアプリケーションにも劣らない堅牢さを発揮する。
次の開発では、ぜひこのパターンをテンプレート化し、あなたの業務ツールを「止まらない、壊れない」プロダクトへ引き上げてほしい。健闘を祈る。
