【C#】WPF と async/await で非同期処理を実行する方法

当ブログでは非同期プログラミングに関しても投稿を行ってきました。非同期プログラミングが最も真価を発揮するのは GUI と絡めて実装を行った場合になりますので、この記事では async / await と GUI を使ってサンプルを紹介していきます。

これまでに紹介してきた非同期の処理については、上記のリストから必要に応じて確認してみてください。

WPF を使った async / await のサンプル

GUI のサンプルとして WPF を使用したサンプルを作成します。Visual Studio で WPF のアプリケーションを選択してプロジェクトを作成していきましょう。作成ができたらメインとなる画面は以下の XAML に書き換えてください。

<Window x:Class="App08.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="150" Width="280">
    <Grid>
        <StackPanel Orientation="Vertical"
                    VerticalAlignment="Center">
            <StackPanel Orientation="Horizontal"
                        HorizontalAlignment="Center"
                        Margin="0,0,0,0">
                <Button Content="同期処理ボタン"
                        Margin="0, 5, 5, 5"
                        Click="Button_Click_Sync"></Button>
                <Button Content="非同期処理ボタン"
                        Margin="5, 5, 5, 5"
                        Click="Button_Click_Async"></Button>
                <Button Content="クリア"
                        Margin="5, 5, 5, 5"
                        Click="Button_Click_Clear"></Button>
            </StackPanel>
            <StackPanel Orientation="Horizontal"
                        HorizontalAlignment="Center"
                        Margin="0, 10, 0, 0">
                <TextBlock x:Name="Message"></TextBlock>
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

サンプル右上には「HTML」となっていますが、XAML が正しいです。上記の XAML にすることで以下の画像のような小さなダイアログを作成できます。タンが 3 つあり、ボタンの下にテキストが表示できるシンプルな画面です。

上記の画面に対して xaml.cs を以下のように記載します。

using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace App08
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        //同期処理のボタン
        private void Button_Click_Sync(object sender, RoutedEventArgs e)
        {
            ClearMessage();
            DoHeavyWork();
            DisplayDoneMessage();
        }

        //非同期処理のボタン
        private async void Button_Click_Async(object sender, RoutedEventArgs e)
        {
            ClearMessage();
            await Task.Run(() => DoHeavyWork());
            DisplayDoneMessage();
        }

        //クリアボタン
        private void Button_Click_Clear(object sender, RoutedEventArgs e)
        {
            ClearMessage();
        }

        private void ClearMessage()
            => this.Message.Text = string.Empty;

        private void DisplayDoneMessage()
            => this.Message.Text = "終了しました。";

        private void DoHeavyWork()
        {
            Thread.Sleep(3000);
        }
    }
}

それぞれ3つのボタンを押下した時のイベントと、簡単なメソッドを2つほど用意しています。特に同期ボタンと非同期ボタンを押下した時の処理についてみていきます。クリアボタンは画面に表示しているメッセージをクリアするだけなので解説は割愛します。

同期処理ボタン押下時の処理

同期処理のボタンを押下した場合は非同期の処理等を実装せずに、通常の手続き型で処理を行うようにしています。DoHeavyWork メソッドは内容を見てもらえればわかると思いますが、 Thread.Sleep メソッドを使用して 3 秒のあいだ処理を止めるようにしています。

//同期処理のボタン
private void Button_Click_Sync(object sender, RoutedEventArgs e)
{
    ClearMessage();
    DoHeavyWork();
    DisplayDoneMessage();
}

アプリケーションを起動させて同期処理のボタンを押下してみると、マウスカーソルが固まることはありませんが、画面上のボタンを押そうとしても反応がない状態となります。もちろん画面を閉じることもできません。

メインスレッドを占有してしまうと、GUI でのコマンドの受付ができなくなってしまい、ローディング画面などを出さないと「画面が固まった」と勘違いされる場合があります。特に時間のかかる処理の場合は勘違いされる可能性も高まります。

非同期ボタンの処理

さて、同期ボタンの処理に対して、非同期ボタンは以下のように実装を行いました。こちらのボタンでは「C# の async / await を使って非同期処理を実行する方法」で紹介した async / await を使って実装を行っています。

//非同期処理のボタン
private async void Button_Click_Async(object sender, RoutedEventArgs e)
{
    ClearMessage();
    await Task.Run(() => DoHeavyWork());
    DisplayDoneMessage();
}

メソッドのシグネチャに async を付与して、 await キーワードを使って非同期処理を実装しました。Tasi.Run() を使用して DoHeavyWork メソッドを起動するようにしています。これは「C# の Task.Run を使って単純なスレッド処理を行わせる方法」で解説した Task.Run() の単純なスレッド処理に該当します。

await メソッドは「C# の async / await を使って非同期処理を実行する方法」で説明している通り、await キーワード以降の処理は同期的に処理されます。これにより Task の処理が終わるまでは、メインスレッドを解放することができ、画面の状態に違和感を感じなくなります。await キーワードを使った Task の処理が完了したあと、処理が完了したことを伝えるメッセージを画面上に表示する処理が動きます。

ボタンを連続で押下してみる

また一度メッセージが表示された後に、それぞれのボタンを押下すると分かることがあります。同期処理のボタンを押下すると、ClearMessage メソッドで画面のテキストをクリアしているのに処理が完了するまでメッセージがクリアされません。

それに対して非同期処理のボタンを押下した場合は、ちゃんとメッセージがクリアされることが分かります。こうした挙動の違いから分かることは、メインスレッドが占有されてしまうと画面上の UI の更新がされずに、重い処理をおこなっている間は画面が固まっているように感じてしまうということです。

async / await のイメージ

async / await の処理は挙動でなんとなく掴めたのではないかと思います。整理するためにも async / await のイメージ図を以下に紹介しておきます。

async メソッド内では、await キーワードが指定された位置でメインスレッドを一度開放して、await に指定された処理が完了したタイミングで後続処理を行うようにスレッド処理を調整してくれます。

上記のイメージ図では、別スレッドで記載したグレーの部分が DoHeavyWork の処理に該当します。DisplayDoneMessage メソッドは処理再開後、スレッドの処理がメインスレッドに戻ってから行われています。

async / await は UI の改善に役立つ

実際に UI と絡めて非同期処理を考えてみると、非常に有用であることがわかります。特に UI はメインスレッドを占有してしまうため、重い処理が実行されると画面が動かなくなると勘違いされる場合があります。

このような事態を招かないためにも、非同期処理をうまく使用してワーカースレッドを活用することで、UI フレンドリーなアプリケーションを開発することが可能となります。ただし、非同期処理は時間軸の制御が難しいため、より多くの考慮が発生するのも事実です。

UI が改善されることも重要ですが、処理内容が繊細な場合は十分な注意を払って実装を行う必要があることは肝に銘じておく必要があります。処理同士を独立させ、互いに依存させないような処理に切り分けることが重要になっていきます。