【実務・中級編】Windows FormsにおけるBackgroundWorkerからValueTaskとIAsyncEnumerableへの移行:ストリーミングデータ処理と非同期イテレーションを駆使したモダンなUIスレッド保護 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

Windows Formsの呪縛を解く:BackgroundWorkerを捨て、ValueTaskとIAsyncEnumerableで実現するモダン非同期ストリーミング

開発現場でいまだに見かける「UIスレッドをブロックする数万件のデータベースクエリ」や、「`BackgroundWorker`の終了をフラグとイベントで必死に同期させようとして爆誕したスパゲッティコード」。
そろそろ、そのレガシーな呪縛から解放されませんか?

こんにちは。チーフアーキテクトの私だ。
今回は、Windows Forms(WinForms)におけるUIスレッド保護の最終解であり、大規模データや外部APIのストリーミング処理における最高峰のパターンを伝授する。

ターゲットは、業務効率化ツールの裏で泣く泣く重い処理をさばいているシニアエンジニアたちだ。
`BackgroundWorker`の時代は終わった。現代のVB.NET(.NET 6/7/8以降)では、`ValueTask`と`IAsyncEnumerable`を駆使して、メモリ割り当てを極限まで削ぎ落とした「真の非同期ストリーミングUI」を構築すべきだ。

なぜその設計が必要なのか、ロジカルかつシャープに解説しよう。

—

1. なぜ従来のBackgroundWorkerやTaskでは不十分なのか?

業務システムでよくある要件:「数百万件のレコード、あるいは巨大なAPIレスポンスを順次取得し、UIのグリッドにリアルタイムでプログレス表示しながら描画したい」。

これを従来の `BackgroundWorker` や単純な `Task.Run` + `List(Of T)` でやろうとすると、以下の致命的な問題に直面する。

1. メモリの無駄撃ち(Allocation Overhead):
全データを一度メモリ上のリストに詰め込んでからUIスレッドに `ReportProgress` で投げ渡す手法は、GC(ガベージコレクター)に甚大な負荷をかける。数万件のオブジェクト生成は瞬く間にGen 0を枯渇させる。
2. スレッドプールの枯渇とUIの硬直:
`Task.Run` の中で同期的なブロッキングI/Oを回すと、スレッドプールを無駄に占有する。
3. カプセル化の崩壊:
進捗報告やキャンセル処理のためのイベント配線が複雑化し、保守性(Maintainability)が完全に死ぬ。

ここで投入するのが、`IAsyncEnumerable(Of T)` による非同期イテレーションと、高頻度呼び出しにおけるヒープ割当をゼロにする`ValueTask`のコンビネーションだ。

—

2. アーキテクチャの全体像

今回構築するアーキテクチャの要件は以下の通り:

  • データプロバイダ(非同期ストリーム): データベースやAPIから、データを1件(またはチャンク単位)ずつ `IAsyncEnumerable` で非同期に「流し込む」。
  • UI層(Windows Forms): `await foreach` を用いて、UIスレッドをブロックせずにデータを逐次受信し、DataGridViewやProgressBarを安全に更新する。
  • キャンセレーション: `CancellationToken` を完全に伝播させ、ユーザーがいつでも処理を安全に中断できるようにする。

—

3. 実装:プロダクションコード

余計な理論はここまでだ。実際の現場でそのまま使える、堅牢でモダンなVB.NETのコードを提示する。

前提条件として、プロジェクトは .NET 6以降(nullable有効、C#ライクな非同期構文がフル活用できる環境) を想定している。

データ取得レイヤー(モック:DBやAPIの代わり)

Imports System.Runtime.CompilerServices
Imports System.Threading
Imports System.Collections.Generic

Public Class DataService

‘ 大規模データを非同期ストリームとして返すメソッド
‘ IAsyncEnumerable を使うことで、データが生成された「その瞬間」に消費側に流せる
Public Async Function FetchLargeDataStreamAsync(
totalRecords As Integer,
CancellationToken As CancellationToken
) As IAsyncEnumerable(Of BusinessRecord)

For i As Integer = 1 To totalRecords
‘ キャンセル要求が来ていないかチェック
CancellationToken.ThrowIfCancellationRequested()

‘ 外部APIやDBからの非同期取得をシミュレート(50msのウェイト)
Await Task.Delay(50, CancellationToken)

‘ 1件ずつ値を「yield」する(メモリ上に全件保持しない)
Yield New BusinessRecord With {
.Id = i,
.Name = $”Record_#{i}”,
.Timestamp = DateTime.Now
}
Next
End Function

End Class

Public Class BusinessRecord
Public Property Id As Integer
Public Property Name As String
Public Property Timestamp As DateTime
End Class

UI層(Windows Formsフォーム)

プログレスバーとDataGridView、そして「開始」「キャンセル」ボタンが配置されているフォームを想定してほしい。
UIスレッドを一切ブロックせず、流れてくるデータを即座に描画する。

Imports System.Threading

Public Class MainForm
Private _cts As CancellationTokenSource
Private ReadOnly _dataService As New DataService()

‘ 処理開始ボタン
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 = 1000 ‘ 仮の総数
DataGridView1.Rows.Clear()

_cts = New CancellationTokenSource()

Try
Dim count As Integer = 0

‘ 【最重要】IAsyncEnumerable を await foreach (VBでは For Await) で安全にイテレート
‘ UIスレッド上で実行されるが、データ待ちの間はスレッドを解放するためフリーズしない
For Await record In _dataService.FetchLargeDataStreamAsync(1000, _cts.Token)

‘ UIにデータを追加
DataGridView1.Rows.Add(record.Id, record.Name, record.Timestamp.ToString(“HH:mm:ss.fff”))

‘ プログレスの更新
count += 1
ProgressBar1.Value = Math.Min(count, ProgressBar1.Maximum)

‘ ユーザービリティのためにグリッドの表示位置を最新に追従
If DataGridView1.Rows.Count > 0 Then
DataGridView1.FirstDisplayedScrollingRowIndex = DataGridView1.Rows.Count – 1
End If
> Next

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の復元
_cts?.Dispose()
_cts = Nothing
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
‘ トークンソースをキャンセル状態にする
_cts?.Cancel()
BtnCancel.Enabled = False
End Sub

End Class

—

4. チーフアーキテクトからの実務アドバイス

このコードをデプロイするにあたり、以下の「現場の罠」に注意してほしい。

1. DataGridViewの描画コスト(Virtual Modeの検討)
もし数万件レベルの描画を `DataGridView1.Rows.Add` で愚直に行うと、UIスレッドが描画処理で悲鳴を上げる(遅延の原因になる)。
もし対象が10万件を超える場合は、`DataGridView` を 仮想モード (VirtualMode = True) に設定し、受け取ったデータを内部の `BindingList(Of T)` やスレッドセーフなコレクションに蓄積しつつ、描画は遅延・バッチ処理させるアプローチをとること。
2. ValueTaskの使い所
今回の例では `IAsyncEnumerable` の内部で非同期処理を行っているが、頻繁に呼び出される非同期メソッド(同期的にキャッシュから即座に結果を返す可能性がある場合など)の戻り値には、`Task` ではなく `ValueTask(Of T)` を用いることで、ヒープ割り当てのオーバーヘッド(GC圧力)をゼロに抑えられる。これを意識するだけで、アプリのメモリフットプリントが劇的に改善する。
3. クロススレッド例外の回避
`For Await` ループは、呼び出し元の同期 contexto(WinFormsの場合はUIスレッドの `WindowsFormsSynchronizationContext`)を自動的にキャプチャするため、ループ内のUI操作(`DataGridView1.Rows.Add` など)で「クロススレッド操作が不正です」という例外が発生しない。これがModern .NETの強力な恩恵だ。

—

結び

レガシーなVB.NETのコードベースをモダンにリファクタリングすることは、単なる「自己満足」ではない。それは、エンドユーザーのストレスを消し去り、システムの寿命を延ばすためのエンジニアリング上の投資だ。

`BackgroundWorker` は過去の遺物としてそっと墓場に眠らせ、`IAsyncEnumerable` と非同期ストリームをあなたの武器庫に加えなさい。

健闘を祈る。

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