長時間かかる非同期処理(ファイルのダウンロード、データ解析、インストール処理など)を実行する際、ユーザーに進捗状況(プログレスバーやパーセンテージ)をフィードバックすることは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スレッド上で実行されます。そのため、InvokeやDispatcherを使わずに、直接プログレスバーやラベルを更新しても安全です。 - コンソールアプリの場合: 特定の同期コンテキストを持たないため、コールバックはスレッドプールのスレッドで実行されることが一般的ですが、サンプルコードのような単純な表示であれば問題なく動作します。
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} を処理中...");
});
