Progress<T>を使ってみた

非同期処理の進捗表示にProgress<T>を使ってみたので、忘れないうちにまとめておく。

とりあえずサンプル。FormにProgressBarをprogressBar1として貼り付けているとして、非同期タスクの処理数をprogressBar1に表示するというもの。

/// <summary>
/// Progres<T>の使用例
/// </summary>
async void progressTest()
{
    //Progress<T>オブジェクトを作成。コンストラクタ引数でイベントハンドラを渡す。
    Progress<int> progress = new Progress<int>(onProgressChanged);

    //非同期タスク開始
    Task task = Task.Run(
        () =>
        {
            //非同期処理にProgress<T>オブジェクトを渡す
            doSometing(progress);
        });

    //非同期タスク完了を待つ
    await task;
}

/// <summary>
/// 非同期タスクの中で実行する処理
/// </summary>
/// <param name="iProgress">Report(T)はIProgress<T>でないと使えない</param>
void doSometing(IProgress<int> iProgress)
{
    for (int done = 1; done <= 100; done++)
    {
        //重たい処理の代わり
        System.Threading.Thread.Sleep(100);

        //進捗報告
        iProgress.Report(done);
    }
}

/// <summary>
/// ProgressChangedイベントのイベントハンドラ
/// </summary>
/// <param name="count">Progress<T>のT</param>
void onProgressChanged(int count)
{
    //渡された達成数をプログレスバーに設定
    progressBar1.Value = count;
}

Progress<T>のTには進捗情報を入れる変数の型を入れる。数値でも文字列でも、自作のクラスでもいい。今回は簡単に作業の完了数(int)だけを通知することにして、Progress<int>とした。

まず、進捗報告をしてもらうProgres<T>のインスタンスを作る。Progress<T>は自身のインスタンスが作成された同期コンテキストを覚えていて、進捗変化を知らせるイベントは、自分が生まれた同期コンテキストに対して行う。なので、イベントを受け取りたい処理の同期コンテキスト(ここではFormの同期コンテキスト)内でインスタンスを作成しないといけない。(別の同期コンテキストでの処理となるTaskの中で作ってしまうとイベントがFormに届かない。)

Progress<T>はコンストラクタ引数にイベントハンドラを取ることができる。その場合、メソッドの型は

void eventHandlerMethod(T) //TはProgress<T>のT

となる。サンプルでは一番下に「onProgressChanged」として作っているので、それをコンストラクタに渡している。

ちなみに、引数なしのコンストラクタでProgress<T>のインスタンスを作成し、そのあとで

Progress<T>.ProgressChanged += eventHandlerMethod;

としてイベントハンドラを登録することもできる。ただ、その場合のイベントハンドラの型は

void eventHandlerMethod(object, T) //第1引数にsenderとなるオブジェクトが必要

としなければいけない。

イベントハンドラについて少し説明しておく。Progressから送られた進捗情報が、引数のTとしてイベントハンドラに渡される。よくあるEventArgsでくるむやり方ではなく、データがそのまま渡されるのが、普通のイベントハンドラとちょっと違う。

進捗報告の準備ができたので非同期タスクを作る。そのとき、非同期タスクの中で進捗報告をおこなってもらうため、準備していたProgress<T>を渡す。サンプルではTask.Runを使ってタスク作成と同時にそのタスクを実行している。そのあとawaitで非同期タスクの完了を待つ。

非同期処理の中でどのように進捗報告するのか、「doSomething」メソッドの処理を見る。まず引数が「Progress<int>」ではなく「IProgress<int>」となっているが、これについては後述。

「doSomething」メソッドには100ミリ秒待機する処理を100回行うように記述している。待機1回をひとつの処理完了と見立てて、1回待つごとに

IProgress<T>.Report(T); //Tに進捗情報を入れる

の形で進捗報告を行う。Report(T)が行われるとProgressChangedイベントが発火し、登録していたイベントハンドラにTが渡されて実行されるという流れになる。

ところで、「Report(T)」メソッドはインターフェース「IProgress<T>」にはあるが、それを継承した「Progress<T>」にはない。(継承先クラスで 明示的なインターフェイスの実装 のみが行われているためだが、なぜそうしているのかはわからない。)なので、Report(T)を使うには、インスタンス作成時やメソッドの引数の宣言でIProgress<T>としておくか、Report(T)を使うときにIProgress<T>にキャストするかしなれけばならない。

以上、備忘録として。

(2016-08-02追記)自分で読み返してもなんかわかりづらかったので、まとめてみた↓

まとめ : Progress<T>の使いかた
  1. 準備
    • 受け取りたい進捗情報の型をTに決める(自作クラスOK)
    • Tを受けとって処理するイベントハンドラを作る
    • 進捗を受け取る同期コンテキスト上でProgress<T>を作り、イベントハンドラを登録する。
  2. 非同期処理を呼び出すとき、作っておいたProgress<T>を渡す
  3. 非同期処理内の進捗を報告したい場所でIProgress<T>.Report(T)する。

参考ページ