【入門編】Windows Formsの「応答なし」を撲滅するIProgress(Of T)とValueTaskの活用:モダンスレッド間通信の完全実装 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

Windows Formsの「応答なし」を撲滅する:IProgress(Of T)とValueTaskによる極限のUI設計

こんにちは。プログラミングの世界へようこそ。
あなたがもし、Excelマクロの自動化から一歩踏み出し、「もっと本格的なWindowsアプリケーションを作りたい」と考え始めたのなら、素晴らしい一歩を踏み出したと言えます。

しかし、VB.NETでツールを作っていると、必ずと言っていいほど直面する「壁」があります。
それは、重い処理を実行した瞬間に画面が真っ白になり、タイトルバーに「(応答なし)」と表示されてしまう現象です。

ユーザーからすれば、アプリがフリーズしたように見えます。これは信頼を失う致命的なミスです。
今日は、この「応答なし」を完全に撲滅し、プロフェッショナルな動きをするアプリケーションを作るための「現代的な非同期設計」を伝授しましょう。

ここをクリアすれば、あなたのコードは「ただ動くもの」から「使いものになる製品」へと進化しますよ。

1. なぜ「応答なし」が起きるのか?(UIスレッドの正体)

まず、敵の正体を知ることから始めましょう。
Windows Formsアプリケーションには、「UIスレッド」という、いわば「専任の受付係」が存在します。

この受付係は、常に以下の仕事をしています。

  • マウスのクリックを検知する
  • ウィンドウを動かす
  • ボタンの色を変える
  • 画面を描画する

問題は、あなたがボタンをクリックした時に「10秒かかる計算」を同じ受付係(UIスレッド)に直接頼んでしまうことです。

受付係が計算に没頭している間、マウスのクリックも画面の更新もすべて後回しにされます。これが「応答なし」のメカニズムです。

解決策のコンセプト

私たちが目指すのは、「受付係(UIスレッド)には、計算の『進捗報告』だけを受け取らせ、実際の計算は『別の作業員(バックグラウンドスレッド)』に任せる」という設計です。

2. 武器を揃える:IProgress(Of T) と ValueTask

今回のミッションを完遂するために、2つの強力な武器を使います。

① IProgress(Of T) :安全な進捗報告の伝言板

通常、バックグラウンドで動いている作業員が「今50%終わったよ!」と直接UI(プログレスバーなど)を操作しようとすると、「スレッド間操作が無効です」というエラーでアプリが強制終了します。UIは非常にデリケートで、受付係以外が勝手に触ることを嫌うからです。

`IProgress(Of T)` は、作業員が「報告」を書き込み、それを受付係が「適切なタイミングで受け取る」ための安全な伝言板の役割を果たします。

② ValueTask :メモリに優しい高速なチケット

これまで非同期処理には `Task` が使われてきました。しかし、`Task` は「オブジェクト」としてメモリ(ヒープ領域)を消費します。
頻繁に呼ばれる処理や、すでに結果が出ているような軽量な非同期処理において、このメモリ消費は蓄積してパフォーマンスを低下させます。

`ValueTask` は、その名の通り「値(Value)」として扱える軽量な仕組みです。メモリの消費を最小限に抑え、極限までパフォーマンスを追求したいプロの現場で重宝されます。

3. 【実践】モダンスレッド間通信の完全実装

それでは、実際に「キャンセル可能」で「進捗が表示される」完璧なコードを見ていきましょう。
以下のコードは、そのままWindows Formsプロジェクトの `Form1.vb` に貼り付けて動作を確認できる設計にしています。

実装コード例

Imports System.Threading

''' <summary>
''' 応答なしを回避する、究極の非同期処理実装例
''' </summary>
Public Class Form1
    ' キャンセル操作を制御するためのトークンソース
    Private _cts As CancellationTokenSource

    ''' <summary>
    ''' 実行ボタンのクリックイベント
    ''' </summary>
    Private Async Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
        ' UIの初期化
        btnStart.Enabled = False
        btnCancel.Enabled = True
        progressBar1.Value = 0
        lblStatus.Text = "処理を開始します..."

        ' 新しいキャンセル信号の準備
        _cts = New CancellationTokenSource()

        ' 進捗報告用のオブジェクトを作成 (IProgressの実装)
        ' これにより、バックグラウンドからUIスレッドへの安全な橋渡しが行われる
        Dim progressReporter As New Progress(Of Integer)(Sub(percent)
                                                              ProgressBar1.Value = percent
                                                              lblStatus.Text = $"進行中... {percent}%"
                                                          End Sub)

        Try
            ' 【ここが肝】重い処理を非同期で呼び出す
            ' Taskではなく、より軽量なValueTaskを返すメソッドを呼び出すイメージ
            Await DoHeavyWorkAsync(progressReporter, _cts.Token)

            lblStatus.Text = "完了しました!"
        Catch ex As OperationCanceledException
            ' キャンセルボタンが押された時の処理
            lblStatus.Text = "ユーザーによって中断されました。"
            progressBar1.Value = 0
        Catch ex As Exception
            ' その他のエラー処理
            MessageBox.Show($"エラーが発生しました: {ex.Message}")
        Finally
            ' 後片付け
            _cts.Dispose()
            btnStart.Enabled = True
            btnCancel.Enabled = False
        End Try
    End Sub

    ''' <summary>
    ''' キャンセルボタンのクリックイベント
    ''' </summary>
    Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click
        ' キャンセル信号を送る
        _cts?.Cancel()
    End Sub

    ''' <summary>
    ''' 重い処理をシミュレートするメソッド
    ''' </summary>
    ''' <param name="progress">進捗を報告するためのインターフェース</param>
    ''' <param name="token">キャンセルを検知するためのトークン</param>
    ''' <returns>ValueTask (メモリ効率の良い非同期戻り値)</returns>
    Private Async Function DoHeavyWorkAsync(progress As IProgress(Of Integer), token As CancellationToken) As ValueTask
        ' 1から100までループする重い処理を模倣
        For i As Integer = 1 To 100
            ' 1. キャンセル要求が来ていないか確認 (これが重要!)
            ' これを忘れると、キャンセルボタンを押しても止まらない「ゾンビ処理」になる
            token.ThrowIfCancellationRequested()

            ' 2. 重い計算の代わり(非同期の待機)
            ' Task.DelayはUIスレッドをブロックせずに「待つ」ことができる
            Await Task.Delay(50, token)

            ' 3. 進捗を報告
            ' IProgressを通じて、安全にUIスレッドへ値を渡す
            progress.Report(i)
        Next
    End Function

End Class

4. 伝説のアーキテクトが教える「陥りやすい罠」

このコードを理解したら、最後にプロとして絶対に避けて通れない「落とし穴」を伝えておきます。

① `Async Void` の禁止令

コードの中で `Async Sub` を使っている箇所がありますね? これはイベントハンドラー(ボタンクリックなど)に限られます。
自作のメソッドを作る際は、必ず `Async Function … As Task` または `As ValueTask` にしてください。
`Async Sub` で作ってしまうと、呼び出し元がその終了を待機できず、エラーが発生した際にアプリがクラッシュ(強制終了)する原因になります。これを「Fire and Forget(撃ちっぱなし)」の罠と呼びます。

② キャンセル確認の「忘れ物」

`CancellationToken` を引数で渡しただけで満足してはいけません。
ループの中で必ず `token.ThrowIfCancellationRequested()` を呼び出してください。これを忘れると、ユーザーが「止まれ!」と言っているのに、裏で計算が走り続ける「ゾンビプロセス」を生み出すことになります。

③ `ValueTask` の使い所

`ValueTask` は非常に強力ですが、何でもかんでも使えばいいわけではありません。

  • Taskを使うべき時: 結果を何度も再利用する場合や、複雑な非同期チェーンを作る場合。
  • ValueTaskを使うべき時: 今回のように、高頻度で呼ばれるループ内や、結果が即座に確定することが多い軽量な非同期処理。

最後に:君はもう、一歩先へ進んでいる

「応答なし」を消し去ることは、単なるテクニックではありません。それは、「ユーザーの時間を尊重する」というエンジニアとしてのマナーです。

今回学んだ `IProgress(Of T)` による安全な通信と、`ValueTask` による効率的なリソース管理。これらを使いこなせるようになれば、あなたの作るアプリケーションは、プロが作ったものと見紛うほどの滑らかさを手に入れます。

最初は難しく感じるかもしれません。でも、コードの意味を一つずつ噛み締めながら書いていけば、必ず君の技術は血肉となります。

さあ、次はどんな素晴らしいツールを作る予定ですか? 応援していますよ。

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