UIスレッドを人質に取るな:VB.NETにおける非同期処理の「解剖学」
多くのVB.NET開発者が、Windows Formsの「応答なし」という呪縛に苦しんでいる。UIスレッドという神聖な領域で、数百メガバイトのCSV解析や大規模なDBトランザクションを実行すれば、OSは容赦なく「このプロセスは死んでいる」と判断を下す。
私は長年、VBAの限界から脱却し、VB.NETへと移行する現場を渡り歩いてきた。そこで共通して見られるのは、「手続き型プログラミングの悪癖」が非同期設計を阻害しているという事実だ。今回は、UIを凍結させることなく、高負荷な処理を安全に制御するための「真の非同期設計」を伝授する。
—
1. 非同期の核心:SynchronizationContextの理解
まず心に刻んでほしい。`Async/Await`は単なる「裏で動かす魔法」ではない。これは、「UIスレッドのコンテキストを保持したまま、タスクの完了を待機する」ための状態機械(State Machine)だ。
VB.NETでは、`Await`キーワードに到達した瞬間、メソッドの実行は一時停止し、制御は呼び出し元(UIスレッド)に戻る。これにより、ユーザーはボタンのクリックやウィンドウの移動が可能になる。処理が完了すれば、コンパイラが自動的にUIスレッドへ戻り、残りの処理を再開させる。この一連のフローを制御する `SynchronizationContext` の存在を忘れてはならない。
—
2. 実装:プログレスバーを伴う堅牢な非同期パターン
ただ「別スレッドで動かせばいい」という考えは素人だ。メモリリークを防ぎ、UIとの安全な連携を行うためのアーキテクチャを提示する。
Imports System.Threading
Imports System.Threading.Tasks
Public Class MainForm
‘ 非同期処理の開始
Private Async Sub btnProcess_Click(sender As Object, e As EventArgs) Handles btnProcess.Click
‘ 連続クリックによる二重実行防止
btnProcess.Enabled = False
ProgressBar1.Value = 0
‘ IProgress(Of T) を用いてスレッドセーフにUIを更新する
Dim progress = New Progress(Of Integer)(Sub(percent)
ProgressBar1.Value = percent
End Sub)
Try
‘ 重い処理をバックグラウンドスレッドへ委譲
‘ Task.Run を使わないと UIスレッドが止まる。これが基本原則。
Await Task.Run(Sub() PerformHeavyFileOperation(progress))
MessageBox.Show(“処理が完了しました。”, “成功”, MessageBoxButtons.OK, MessageBoxIcon.Information)
Catch ex As Exception
‘ 非同期例外の捕捉
MessageBox.Show($”エラー発生: {ex.Message}”, “異常終了”, MessageBoxButtons.OK, MessageBoxIcon.Error)
Finally
btnProcess.Enabled = True
End Try
End Sub
‘ 実際の重い処理(UIスレッドには決して触れさせない)
Private Sub PerformHeavyFileOperation(progress As IProgress(Of Integer))
For i As Integer = 1 To 100
‘ ここで本来のファイルI/Oや計算を行う
Thread.Sleep(50) ‘ ダミーの負荷
‘ 進捗を報告
progress.Report(i)
Next
End Sub
End Class
—
3. シニアエンジニアが意識すべき「深淵」
この実装を「動いた」で終わらせてはいけない。以下の3点こそが、システムを堅牢にする。
1. リソースの明示的解放(Dispose)
.NETのGC(ガベージコレクタ)に頼り切る設計は、高負荷環境では破綻する。特に `FileStream` や `StreamReader` などのアンマネージリソースを扱う際は、`Using` ブロックを徹底せよ。非同期処理の中でも、寿命の短いオブジェクトは即座に破棄する癖をつけなければならない。
2. 進捗報告のオーバーヘッド
`progress.Report` を1ミリ秒単位で呼び出すような設計は、メッセージキューを溢れさせ、かえってUIの応答性を悪化させる。UIの更新頻度は人間が認識できる限界(概ね20〜30fps、つまり30〜50ms程度)に制限する「サンプリング」の技術が必要だ。
3. レガシーAPIとの連携
Win32 APIを直接呼び出す必要がある場合、`Task.Run` の内部で `P/Invoke` を行う際は注意が必要だ。特定のAPIは「呼び出したスレッド」でしか動作しない制約を持つものがある。その場合、単純な `Task.Run` ではなく、`STA(Single Thread Apartment)` のスレッドプールを明示的に管理しなければならない。
—
結論:VB.NETを「使い倒す」ということ
VB.NETは古臭い言語ではない。正しい作法を知る者にとって、極めて強力な開発生産性を提供するツールだ。
「UIがフリーズする」という問題は、コードの書き方が悪いのではなく、「スレッドの所有権」に対する意識の欠如が原因だ。今回示した `Async/Await` と `IProgress(Of T)` による疎結合な設計は、今後のWindows Forms開発の基盤となるはずだ。
コードは嘘をつかない。あなたの書く一行一行が、システムの寿命を左右するのだ。次にコードを書くとき、その裏で何が動いているのかを想像してほしい。それこそが、伝説のエンジニアへの第一歩となる。
