tips - async/await による非同期処理 その 1

 非同期処理をおこなうための方法はいくつかありますが、C# 5.0 以降では async/await 演算子を用いることで非同期処理に関する記述が簡潔になりました。 ここでは async/await 演算子の基本的な使い方について紹介します。

 サンプルプロジェクトはここからダウンロードできます。

ページ内リンク

概要

 ここでは次のような UI のアプリケーションを作成します。

Fig.1 : サンプルアプリケーションの外観
このアプリケーションは、ボタンを押すと重たい処理を実行しますが、 この処理は非同期処理されるため、 UI がフリーズすることなく動作するようになっています。 したがって、ボタンを押した直後も下図のようにチェックボックス等の操作ができます。
Fig.2 : 処理中でも UI 操作ができる
ただし、連続して処理が開始されないように、 処理中はボタン操作できないようにします。

サンプル UI の作成

 上記のサンプル UI の XAML は次のようになります。

<Window x:Class="AsyncSample.Views.MainView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainView" Height="300" Width="300">
    <StackPanel>
        <Button Content="Click me!" Command="{Binding ButtonCommand}" />
        <TextBlock Text="{Binding Result}" />
        <TextBox Text="テキスト入力" />
        <CheckBox Content="Check me!" />
    </StackPanel>
</Window>
Code 1 : サンプル UI の XAML コード
StackPanel コントロールで各コントロールを並べているだけです。 ボタンを押したときに処理を開始させたいので、 Button コントロールには ButtonCommand プロパティを、 処理が実行されているかどうかをわかるようにしたいので、 TextBlcok コントロールには Result プロパティをそれぞれデータバインドしています。 詳細は次項で説明します。

ViewModel の作成 (同期処理として動作)

 上記のサンプル UI でデータバインド設定した ButtonCommand プロパティおよび Result プロパティを実装した ViewModel を次のように定義します。

namespace AsyncSample.ViewModels
{
    using System.Threading;

    public class MainViewModel : NotificationObject
    {
        #region 公開プロパティ
        private string result = "まだ実行してないよ。";
        /// <summary>
        /// 実行結果を取得または設定します。
        /// </summary>
        public string Result
        {
            get { return result; }
            set { SetProperty(ref result, value); }
        }

        private bool isBusy;
        /// <summary>
        /// ビジー状態かどうかを取得または設定します。
        /// </summary>
        public bool IsBusy
        {
            get { return isBusy; }
            set
            {
                if (SetProperty(ref isBusy, value))
                    ButtonCommand.RaiseCanExecuteChanged();
            }
        }

        private DelegateCommand buttonCommand;
        /// <summary>
        /// ボタンコマンドを取得します。
        /// </summary>
        public DelegateCommand ButtonCommand
        {
            get
            {
                return buttonCommand ?? (buttonCommand = new DelegateCommand(
                    _ =>
                    {
                        System.Console.WriteLine("重たい処理をコールします。");
                        HeavyWork();
                        System.Console.WriteLine("重たい処理をコールしました。");
                    },
                    _ =>
                    {
                        return !IsBusy;
                    }));
            }
        }
        #endregion 公開プロパティ

        /// <summary>
        /// 重たい処理を実行します。
        /// </summary>
        private void HeavyWork()
        {
            IsBusy = true;
            Result = "只今実行中...";
            System.Console.WriteLine("重たい処理を実行します。");

            // 何か重たい処理
            Thread.Sleep(3000);

            System.Console.WriteLine("重たい処理を実行しました。");
            Result = "終了しました。";
            IsBusy = false;
        }
    }
}
Code 2 : 重たい処理を同期的に処理する
Result プロパティは重たい処理の前後に書き換えられ、 現在の状態をテキストで表示させます。 IsBusy プロパティは重たい処理実行中に true になるようにします。 そして ButtonCommand プロパティでボタンが押されたときの処理およびボタンの有効/無効判定をおこないます。

 ここでは重たい処理を HeavyWork() メソッドで表現しています。 このメソッドでは重たい処理を仮想的に表現するために Sleep() メソッドで 3[s] 間待機するようにしています。 また、その前後で Result プロパティおよび IsBusy プロパティを変更することで、 重たい処理を実行しているときとそうでないときを区別しようとしています。

 しかし、このままでは重たい処理は同期的に処理されるため、ボタンが押されるとこの処理が終わるまで応答が返ってこなくなってしまいます。

Fig.3 : ボタンを押すと UI がフリーズしたように見える
出力ウィンドウにも
重たい処理をコールします。
重たい処理を実行します。
重たい処理を実行しました。
重たい処理をコールしました。
と表示され、すべて順番に実行されている様子がわかります。

 また、重たい処理の前後で Result プロパティや IsBusy プロパティを変更していますが、 その変化は UI に反映されていません。 これもやはり重たい処理の部分でプログラムが返ってこないためです。 そして、プログラムが返ってきたときにはもう Result プロパティは "終了"、 IsBusy プロパティは false に戻ってしまうため、 UI の見た目には実行中の動作は現れてきません。

非同期処理に書き直した ViewModel

 それでは上記のコードを非同期処理に書き直します。 先ほどの HeavyWork() メソッドを HeavyWorkAsync() メソッドからコールする形にします。

/// <summary>
/// 重たい処理を非同期で実行します。
/// </summary>
private async void HeavyWorkAsync()
{
    IsBusy = true;
    Result = "只今実行中...";
    System.Console.WriteLine("Thread[{0}] 非同期処理を実行します。", Thread.CurrentThread.ManagedThreadId);
    await HeavyWork();
    // ここで一旦 return される

    // 非同期処理が終了したらここから再開する
    System.Console.WriteLine("Thread[{0}] 非同期処理を実行しました。", Thread.CurrentThread.ManagedThreadId);
    Result = "終了しました。";
    IsBusy = false;
}

/// <summary>
/// 重たい処理を実行するタスクを返します。
/// </summary>
/// <returns>重たい処理を実行するタスク</returns>
private Task HeavyWork()
{
    return Task.Run(() =>
    {
        System.Console.WriteLine("Thread[{0}] 重たい処理を実行します。", Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(3000);
        System.Console.WriteLine("Thread[{0}] 重たい処理を終了します。", Thread.CurrentThread.ManagedThreadId);
    });
}
Code 3 : 重たい処理を非同期処理する HeavyWorkAsync() メソッド
await 演算子を付けてコールすることで、 そのメソッドを非同期処理することを意味します。 また、await 演算子を使っているということを宣言するために、 HeavyWorkAsync() メソッドに async 修飾子を付けています。

 await 演算子を使ってコールされるメソッドは必ず System.Threading.Tasks.Task クラスを返す必要があります。 これは、await 演算子によってメソッドを実行するのではなくタスクを実行するためです。 このため、HeavyWork() メソッドの戻り値を Task クラスとし、 その中身は Task.Run() メソッドを使って、 スレッドプール上に配置するタスクのタスクハンドルを返すようにします。 実際の処理は Run() メソッドの中にデリゲートの形で記述します。

 最後に、HeavyWorkAsync() メソッドを呼ぶように ButtonCommand プロパティの中身を書き換えます。

private DelegateCommand buttonCommand;
/// <summary>
/// ボタンコマンドを取得します。
/// </summary>
public DelegateCommand ButtonCommand
{
    get
    {
        return buttonCommand ?? (buttonCommand = new DelegateCommand(
            _ =>
            {
                System.Console.WriteLine("Thread[{0}] 非同期処理をコールします。", Thread.CurrentThread.ManagedThreadId);
                HeavyWorkAsync();
                System.Console.WriteLine("Thread[{0}] 非同期処理をコールしました。", Thread.CurrentThread.ManagedThreadId);
            },
            _ =>
            {
                return !IsBusy;
            }));
    }
}
Code 4 : HeavyWorkAsync() メソッドをコールするようにする

 以上の変更によって、Fig.2 に示したように重たい処理が非同期処理されるようになります。 また、出力ウィンドウへの出力は

Thread[10] 非同期処理をコールします。
Thread[10] 非同期処理を実行します。
Thread[12] 重たい処理を実行します。
Thread[10] 非同期処理をコールしました。
Thread[12] 重たい処理を終了します。
Thread[10] 非同期処理を実行しました。
となり、重たい処理をコールし終わったメッセージが、 重たい処理が終了する前に出力されている様子がわかります。 また、スレッドの ID が非同期処理をコールした側と、 実際に重たい処理を実行している側で異なっていることが確認できます。

上記で示しているコードの中のコメントにも書いてあるように、 async/await による非同期処理をおこなった場合、 それぞれのスレッドで実行されるコードの順番が少し複雑になります。 ここで紹介しているコードと実行結果のスレッド ID を照らし合わせながら 順に追って把握しておくことをお勧めします。

Designed by CSS.Design Sample