2026年版 WinUIアプリ開発 Tips:UIを固めない非同期処理―async/awaitとDispatcherQueueの正しい使い分け

スポンサーリンク

WinUI 3でファイル読み込みやAPI通信、データ集計などを追加したとき、「処理中にウィンドウが固まる」「ボタンを押しても反応しない」という問題に出会うことがあります。処理自体は正しくても、UIスレッドを長時間占有すると、アプリはユーザーから見ると停止したように見えます。

今回は、WinUI 3でUIを応答可能な状態に保つための基本として、async/await、Task.Run、DispatcherQueueの役割を整理します。単に「非同期にすれば速くなる」と覚えるのではなく、I/O待ちとCPU負荷の高い処理を分けて考えるのがポイントです。

なぜUIが固まるのか

WinUI 3の画面操作は基本的にUIスレッドで処理されます。ボタンのクリック、レイアウト、入力への応答なども同じスレッドが担当します。ここで数秒かかる処理を同期的に実行すると、その間UIスレッドは次の仕事を処理できません。

たとえばボタンクリックで大きなファイルを同期読み込みし、そのまま大量の計算を行うと、処理が終わるまで画面更新や入力処理が滞ります。Microsoftの公式ガイドでも、アプリの応答性はUIスレッドが処理可能な状態にあるかどうかに関係すると説明されています。

大切なのは、「重い処理は全部Task.Runへ入れる」という単純なルールではありません。ネットワークやファイルの完了を待つI/O処理と、CPUを使い続ける計算処理では対処が異なります。

I/O待ちはasync/awaitを第一候補にする

HTTP通信や非同期ファイル読み込みのように、処理時間の多くが外部処理の完了待ちなら、非同期APIをawaitするのが基本です。

private async void LoadButton_Click(object sender, RoutedEventArgs e)
{
    LoadButton.IsEnabled = false;
    StatusText.Text = "読み込み中...";

    try
    {
        string text = await File.ReadAllTextAsync("sample.txt");
        ResultTextBox.Text = text;
        StatusText.Text = "読み込み完了";
    }
    catch (Exception ex)
    {
        StatusText.Text = $"読み込みに失敗しました: {ex.Message}";
    }
    finally
    {
        LoadButton.IsEnabled = true;
    }
}

awaitしている間、UIスレッドを同期的に待機させ続ける必要がありません。処理完了後は通常、イベントハンドラーの続きへ戻り、UIを更新できます。ここで重要なのは、async/awaitが処理そのものを高速化する魔法ではないことです。待ち時間の間にUIスレッドを塞がないことが主な目的です。

.Resultや.Wait()で待たない

非同期APIを使っていても、次のように同期的に結果を取り出すと利点を失います。

// UIイベント内では避けたい例
string text = File.ReadAllTextAsync("sample.txt").Result;

// これも同期的に待機する
SomeAsyncMethod().Wait();

.Resultや.Wait()は呼び出したスレッドをブロックします。特にUI処理では応答停止や、処理の組み合わせによってはデッドロックにつながるため、非同期APIは可能な限りawaitでつなげます。WebView2の公式ドキュメントでも、非同期操作を.Resultや.Wait()で待つと内部のメッセージ処理を妨げ、デッドロックにつながるため避けるよう明記されています。

CPUを使う重い計算はTask.Runを検討する

一方、画像解析、大量データの集計、複雑な変換など、待っているのではなくCPUを長時間使う処理は事情が違います。こうした処理をUIスレッドで実行すると、awaitする対象がないため画面が固まります。

UIと切り離せる純粋な計算であれば、Task.Runでスレッドプールへ移す方法を検討できます。

private async void CalculateButton_Click(object sender, RoutedEventArgs e)
{
    CalculateButton.IsEnabled = false;
    StatusText.Text = "集計中...";

    try
    {
        var result = await Task.Run(() => CalculateReport());
        ResultTextBlock.Text = result.ToString();
        StatusText.Text = "集計完了";
    }
    finally
    {
        CalculateButton.IsEnabled = true;
    }
}

private int CalculateReport()
{
    // UI要素には触れず、計算だけを行う
    return Enumerable.Range(1, 5_000_000)
                     .Where(x => x % 3 == 0)
                     .Sum(x => x % 1000);
}

ポイントはCalculateReportの中からTextBlockやButtonなどのUI要素を操作しないことです。バックグラウンド側は計算結果を返し、UI更新はawait後に行う、と責務を分けると安全です。

DispatcherQueueは何のために使うのか

バックグラウンドスレッドで発生したイベントなど、最初からUIスレッド外にいる処理から画面を更新したい場合にはDispatcherQueueを使います。WinUI 3では、UWPで使われていたCoreDispatcherではなくDispatcherQueueを使うことがMicrosoftの公式ガイドで案内されています。

private void OnBackgroundProgress(int percent)
{
    DispatcherQueue.TryEnqueue(() =>
    {
        ProgressText.Text = $"{percent}%";
    });
}

TryEnqueueは、「このUI更新をUIスレッド側のキューへ入れてください」と依頼する役割です。つまりTask.RunとDispatcherQueueは競合する仕組みではありません。前者はCPU負荷の高い仕事をUIスレッドから外すため、後者は別スレッドからUI側へ安全に仕事を戻すために使います。

Before / After:よくある実装を整理する

改善前の典型例は、クリックイベントの中で「ファイルを同期読み込み→大量計算→UI更新」をすべて順番に実行する形です。この場合、全工程をUIスレッドが抱えます。

改善後は、I/Oには非同期APIとawaitを使い、CPU負荷が大きい計算だけを必要に応じてTask.Runへ分離します。そしてバックグラウンドから通知される進捗など、UIスレッド外から画面へ触れる必要がある場合だけDispatcherQueueを使います。

この切り分けをすると、「とりあえず全部Task.Run」という実装を避けられます。非同期I/OをさらにTask.Runで包んでも、通常はI/O待ちそのものが速くなるわけではありません。

実務ではキャンセルも考える

数秒以上かかる処理では、画面を固めないだけでなく、ユーザーが途中で止められる設計も重要です。.NETの非同期APIにはCancellationTokenを受け取れるものが多くあります。長時間処理をサービスクラスへ分離し、UI側からキャンセルトークンを渡せる形にしておくと、キャンセルボタンや画面遷移時の中断へ発展させやすくなります。

また、処理中に同じボタンを何度も押せると二重実行が起きる場合があります。サンプルのように開始時にボタンを無効化し、finallyで戻すだけでも事故を減らせます。実務では例外処理、キャンセル、二重実行防止をセットで考えると安定します。

初心者が間違えやすい3つのポイント

1つ目は「asyncを付ければ自動的に別スレッドになる」と考えることです。async/awaitとスレッドの切り替えは同じ概念ではありません。非同期I/Oは、待ち時間にスレッドを占有しないことに価値があります。

2つ目は「UIが重いなら何でもTask.Run」です。UI要素はUIスレッドで扱う必要があるため、XAML要素をバックグラウンドへ持ち込む設計は避けます。

3つ目は「DispatcherQueueを使えば重い処理が軽くなる」と考えることです。DispatcherQueueへ重い計算そのものを積めば、結局UIスレッドで実行されます。キューへ戻すのは、結果表示など短いUI操作に絞るのが基本です。

Windows App SDKのバージョンについて

この記事の中心であるasync/await、Task.Run、DispatcherQueueの考え方は、特定の最新リリースだけに依存する新機能ではありません。ただしWindows App SDKは継続的に更新されており、2026年には2.x系の安定版も公開されています。新規プロジェクトや更新時には、利用するWindows App SDKのサポート状況と最新の安定版リリースノートを公式情報で確認してください。

まとめ

WinUI 3でUIを固めないためには、処理の種類を見分けることが重要です。ファイルやネットワークなどのI/O待ちは非同期APIをawaitし、CPUを長時間使う計算は必要に応じてTask.Runへ分離します。バックグラウンド側からUI更新が必要なときはDispatcherQueue.TryEnqueueでUIスレッドへ戻します。

「非同期処理」という1つの言葉でまとめず、待っているのか、計算しているのか、UIへ戻したいのかを分けて考えると実装方針が見えやすくなります。応答性の高いWinUI 3アプリを作るときに、まず押さえておきたい設計の土台です。

参考資料

Please follow and like us:

コメント

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