【C#】非同期処理の進捗状況をProgressで通知する方法

長時間かかる非同期処理(ファイルのダウンロード、データ解析、インストール処理など)を実行する際、ユーザーに進捗状況(プログレスバーやパーセンテージ)をフィードバックすることはUX上非常に重要です。

C#には、非同期タスク側から呼び出し元(UIスレッドやメイン処理)へ安全に進捗を報告するための標準パターンとして IProgress<T> インターフェースと Progress<T> クラスが用意されています。これらを使用することで、スレッド間通信の複雑さを隠蔽し、疎結合な設計が可能になります。

目次

Progress<T>を使用した進捗報告の実装

以下のコードは、バックグラウンドで行われるデータ移行処理の進捗率を、メインコンソールにリアルタイムで表示するシミュレーションです。

Progress<T> のコンストラクタにコールバック(進捗が報告された時の処理)を登録し、非同期メソッド側では IProgress<T>.Report() を呼ぶだけで通知が完了します。

サンプルコード

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

public class Program
{
    public static async Task Main()
    {
        Console.WriteLine("--- データ移行処理を開始します ---");

        // 1. 進捗報告を受け取るための Progress<T> インスタンスを作成
        // コンストラクタ引数に「報告を受けた時に実行したい処理」を記述します。
        // GUIアプリ(WPF/WinForms)の場合、このラムダ式は自動的にUIスレッドで実行されます。
        var progressIndicator = new Progress<int>(percent =>
        {
            // 進捗状況を表示(カーソル位置を戻して上書き表示)
            Console.Write($"\r現在: {percent}% 完了");
        });

        // 処理クラスのインスタンス化
        var migrator = new DataMigrator();

        // 2. 非同期メソッドに進捗レポーター(IProgress<T>)を渡して実行
        // ※nullを渡しても動作するように設計するのが一般的です
        await migrator.RunMigrationAsync(totalItems: 50, progress: progressIndicator);

        Console.WriteLine("\n--- 全ての処理が完了しました ---");
    }
}

/// <summary>
/// 実際の重い処理を行うクラス
/// </summary>
public class DataMigrator
{
    /// <summary>
    /// データ移行を非同期で実行し、進捗を報告する
    /// </summary>
    /// <param name="totalItems">処理するデータ総数</param>
    /// <param name="progress">進捗報告先(オプション)</param>
    public async Task RunMigrationAsync(int totalItems, IProgress<int> progress = null)
    {
        // CPUバウンドな処理として別スレッドで実行
        await Task.Run(async () =>
        {
            for (int i = 1; i <= totalItems; i++)
            {
                // 1. 重い処理のシミュレーション(データベースへの書き込みなど)
                await Task.Delay(50); // 50ms待機

                // 2. 進捗の報告
                if (progress != null)
                {
                    // 現在の進捗率を計算
                    int percentComplete = (i * 100) / totalItems;
                    
                    // Reportメソッドを呼ぶことで、Main側のコールバックが発火する
                    progress.Report(percentComplete);
                }
            }
        });
    }
}

解説と技術的なポイント

1. IProgress<T> と Progress<T> の役割分担

  • IProgress<T>: 「進捗を報告する機能」だけを定義したインターフェースです。非同期メソッドの引数として渡します。これにより、メソッド側は「誰が」「どのように」進捗を表示するかを知る必要がなくなります。
  • Progress<T>: IProgress<T> の標準実装クラスです。呼び出し元(MainメソッドやUI)で使用します。

2. スレッドコンテキストの自動同期(重要)

Progress<T> の最大の特徴は、「インスタンスが作成された時の同期コンテキスト(SynchronizationContext)をキャプチャする」 という点です。

  • GUIアプリ(WPF/Windows Forms)の場合: UIスレッド上で new Progress<T>(...) を行うと、コールバック(ラムダ式内)も自動的にUIスレッド上で実行されます。そのため、InvokeDispatcher を使わずに、直接プログレスバーやラベルを更新しても安全です。
  • コンソールアプリの場合: 特定の同期コンテキストを持たないため、コールバックはスレッドプールのスレッドで実行されることが一般的ですが、サンプルコードのような単純な表示であれば問題なく動作します。

3. Tの型について

サンプルでは int(パーセンテージ)を使用しましたが、T には任意の型を指定できます。例えば、以下のようなクラスを定義すれば、より詳細な情報を通知できます。

public class ProgressReport
{
    public int Percentage { get; set; }
    public string CurrentFileName { get; set; }
}

// 使用例
var p = new Progress<ProgressReport>(report => 
{
    Console.WriteLine($"{report.Percentage}% - {report.CurrentFileName} を処理中...");
});
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次