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

 重たい処理をおこなう場合に非同期処理をよく用いますが、 その処理の進捗状況がわからないと、 本当に処理が実行されているのかどうかがわかりません。 ここでは async/await 演算子による非同期処理で進捗状況を取得する例を紹介します。

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

ページ内リンク

概要

 ここでは「async/await による非同期処理 その 1」で示したサンプルプログラム の UI 部分をそのまま流用します。

Fig.1 : サンプルアプリケーションの外観

基本的な非同期処理の記述

 非同期処理の記述は「async/await による非同期処理 その 1」で示したサンプルプログラムとほぼ同じですが、 一部だけ変更したいと思います。

 まず、ViewModel が公開するプロパティは以下のようにそのまま流用します。

namespace AsyncSample3.ViewModels
{
    using System;
    using System.Threading;
    using System.Threading.Tasks;

    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("Thread[{0}] 非同期処理をコールします。", Thread.CurrentThread.ManagedThreadId);
                        HeavyWorkAsync();
                        System.Console.WriteLine("Thread[{0}] 非同期処理をコールしました。", Thread.CurrentThread.ManagedThreadId);
                    },
                    _ =>
                    {
                        return !IsBusy;
                    }));
            }
        }
        #endregion 公開プロパティ
    }
}
Code 1 : ViewModel の公開プロパティ

 そして、ButtonCommand プロパティから呼ばれる HeavyWorkAsync() メソッドもそのまま流用します。

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

    // 非同期処理が終了したらここから再開する
    System.Console.WriteLine("Thread[{0}] 非同期処理を実行しました。", Thread.CurrentThread.ManagedThreadId);
    Result = "終了しました。";
    IsBusy = false;
}
Code 2 : 重たい処理を非同期処理する HeavyWorkAsync() メソッドもそのまま流用

 ここからが少し違います。上記コードで HeavyWork() メソッドをコールしていた部分が HeavyWorkTask() メソッドをコールしていますね。 HeavyWorkTask() メソッドは次のようなコードになります。

/// <summary>
/// 重たい処理を実行するタスクを返します。
/// </summary>
/// <returns>重たい処理を実行するタスク</returns>
private Task HeavyWorkTask()
{
    return Task.Run(() =>
    {
        // 重たい処理を実行
        System.Console.WriteLine("Thread[{0}] 重たい処理を実行します。", Thread.CurrentThread.ManagedThreadId);
        HeavyWork();
        System.Console.WriteLine("Thread[{0}] 重たい処理を終了します。", Thread.CurrentThread.ManagedThreadId);
    });
}

/// <summary>
/// 重たい処理を実行します。
/// </summary>
private void HeavyWork()
{
    int count = 0;
    while (count++ < 100)
    {
        // 重たい処理
        Thread.Sleep(30);
    }
}
Code 3 : 非同期処理のためのメソッド

 このままでは単に HeavyWork() メソッドを非同期処理するだけで、 その進捗状況を知ることができません。次は進捗状況を取得できるように Code 3 のメソッドを変更していきます。

進捗状況を報告するようにする

 進捗状況を報告するには IProgress<T> インターフェースを使用します。 HeavyWork() メソッドを次のように変更します。

/// <summary>
/// 重たい処理を実行します。
/// </summary>
/// <param name="progress">進捗を示すための <code>System.IProgress&lt;int&gt;</code> を実装したクラスを指定します。</param>
private void HeavyWork(IProgress<int> progress)
{
    int count = 0;
    while (count++ < 100)
    {
        // 重たい処理
        Thread.Sleep(30);

        // 進捗率の更新
        progress.Report(count);
    }
}
Code 4 : IProgress<T> インターフェースによる進捗報告
IProgress<T> には Report() メソッドがあり、 このメソッドに進捗を示すパラメータを渡すことで進捗状況を報告します。 つまり、進捗が進んだら必ず Report() メソッドをコールする必要があります。

 続いて、HeavyWork() メソッドを呼び出す HeavyWorkTask() メソッドを次のように変更します。

/// <summary>
/// 重たい処理を実行するタスクを返します。
/// </summary>
/// <returns>重たい処理を実行するタスク</returns>
private Task HeavyWorkTask()
{
    return Task.Run(() =>
    {
        // 進捗報告用クラスのインスタンス生成
        var p = new Progress<int>(i =>
        {
            // 進捗報告は UI スレッドで実行されるため、
            // データバインドされたプロパティも操作できる
            Result = "只今実行中..." + " [" + i.ToString() + "%]";
        });

        // 重たい処理を実行
        System.Console.WriteLine("Thread[{0}] 重たい処理を実行します。", Thread.CurrentThread.ManagedThreadId);
        HeavyWork(p);
        System.Console.WriteLine("Thread[{0}] 重たい処理を終了します。", Thread.CurrentThread.ManagedThreadId);
    });
}
Code 5 : Progress<int> クラスによる進捗率更新
HeavyWork() メソッドに入力引数を追加したので、 その引数を指定するコードが追加されています。

 Progress<T> クラスのコンストラクタでは、 Report() メソッドがコールされたときに実行される処理を指定します。 このとき、その入力引数は Report() メソッドをコールしたときに渡した T 型のパラメータとなります。 ここでは Result プロパティを、この入力引数を含めた形で変更しています。

 通常、UI 更新に関連するプロパティ等は UI スレッド上からのみアクセスが許可されているため、 例えばデータバインド機能によって UI と連携している Result プロパティは、 UI スレッド以外で実行されている HeavyWork() メソッド内からは操作できず、 仮に操作しようとすると例外が発生してしまいます。 しかし、IProgress<T> の Report() メソッドは、 その指定された処理の実体は UI スレッド上で処理されるため、 上記のように Result プロパティにアクセスすることができます。

 進捗率の計算に関してはそれぞれのアプリケーションによって異なります。 今回のサンプルでは、1 〜 100 までの数値を Report() メソッドに渡し、 これを進捗率として [%] で表記しましたが、 Report() メソッドで渡す数値が [%] そのものではないかもしれないし、 そもそも処理の全体を知っているのは HeavyWork() メソッドではなく Progress クラスのインスタンスを持つクラスのほうかもしれません。 この部分はそれぞれのアプリケーションに応じて変更することになります。

Fig.2 : 実行結果

Designed by CSS.Design Sample