[C#] How to Report Progress in Asynchronous Methods Using Progress

When executing long-running asynchronous tasks (such as file downloads, data analysis, or installation processes), providing feedback on progress (via progress bars or percentages) is crucial for a good User Experience (UX).

C# provides the IProgress<T> interface and the Progress<T> class as standard patterns for safely reporting progress from an asynchronous task to the caller (such as the UI thread or main process). Using these tools hides the complexity of inter-thread communication and enables a loosely coupled design.

目次

Implementing Progress Reporting with Progress<T>

The following code simulates a background data migration process where the progress rate is displayed in real-time on the main console.

You simply register a callback (the action to perform when progress is reported) in the constructor of Progress<T>, and call IProgress<T>.Report() within the asynchronous method to trigger the notification.

Sample Code

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

public class Program
{
    public static async Task Main()
    {
        Console.WriteLine("--- Starting Data Migration ---");

        // 1. Create a Progress<T> instance to receive progress reports
        // The constructor argument defines the action to execute when a report is received.
        // In GUI apps (WPF/WinForms), this lambda automatically runs on the UI thread.
        var progressIndicator = new Progress<int>(percent =>
        {
            // Display progress (Overwrite the current line by moving the cursor back)
            Console.Write($"\rCurrent: {percent}% Completed");
        });

        // Instantiate the processing class
        var migrator = new DataMigrator();

        // 2. Pass the progress reporter (IProgress<T>) to the async method
        // * It is common design to ensure the method works even if null is passed
        await migrator.RunMigrationAsync(totalItems: 50, progress: progressIndicator);

        Console.WriteLine("\n--- All processes completed ---");
    }
}

/// <summary>
/// Class that performs the actual heavy processing
/// </summary>
public class DataMigrator
{
    /// <summary>
    /// Executes data migration asynchronously and reports progress
    /// </summary>
    /// <param name="totalItems">Total number of items to process</param>
    /// <param name="progress">Target for progress reporting (optional)</param>
    public async Task RunMigrationAsync(int totalItems, IProgress<int> progress = null)
    {
        // Execute on a separate thread as a CPU-bound operation
        await Task.Run(async () =>
        {
            for (int i = 1; i <= totalItems; i++)
            {
                // 1. Simulate heavy processing (e.g., writing to a database)
                await Task.Delay(50); // Wait 50ms

                // 2. Report progress
                if (progress != null)
                {
                    // Calculate current progress percentage
                    int percentComplete = (i * 100) / totalItems;
                    
                    // Calling the Report method triggers the callback in Main
                    progress.Report(percentComplete);
                }
            }
        });
    }
}

Explanation and Technical Points

1. Roles of IProgress<T> and Progress<T>

  • IProgress<T>: An interface that defines only the “ability to report progress.” It is passed as an argument to asynchronous methods. This ensures the method does not need to know who displays the progress or how it is displayed.
  • Progress<T>: The standard implementation class of IProgress<T>. It is used by the caller (Main method or UI).

2. Automatic Synchronization of Thread Context (Important)

The most significant feature of Progress<T> is that it captures the SynchronizationContext present when the instance is created.

  • GUI Apps (WPF/Windows Forms): If you create new Progress<T>(...) on the UI thread, the callback (inside the lambda) will automatically execute on the UI thread. This allows you to safely update progress bars or labels without manually using Invoke or Dispatcher.
  • Console Apps: Since they do not have a specific synchronization context, the callback typically runs on a thread pool thread, but this works fine for simple console output as shown in the sample.

3. About the Type T

In the sample, we used int (percentage), but T can be any type. For example, you can define a custom class to report more detailed information:

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

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

この記事を書いた人

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

目次