完了時の非同期タスクの処理 (C#)

Task.WhenAny を使用すると、複数のタスクを、開始された順番に処理するのでなく、同時に開始して完了するごとに 1 つずつ処理できます。

クエリを使用して、タスクのコレクションを作成する例を次に示します。 各タスクは、指定された Web サイトのコンテンツをダウンロードします。 while ループの各反復で、待機されている WhenAny への呼び出しは、最初にダウンロードを終了するタスクのコレクションにあるタスクを返します。 タスクはコレクションから削除され、処理されます。 ループは、コレクションのタスクがなくなるまで繰り返されます。

前提条件

次のいずれかのオプションを使用してこのチュートリアルを進めることができます。

  • .NET デスクトップ開発ワークロードをインストールした Visual Studio 2022。 このワークロードを選択すると、.NET SDK が自動的にインストールされます。
  • .NET SDKVisual Studio Code などの任意のコード エディター。

サンプル アプリケーションの作成

新しい .NET Core コンソール アプリケーションを作成します。 dotnet new console コマンドを使用するか、Visual Studio を使用して作成できます。

岡津位のエディターで Program.cs を開き、既存のコードをこのコードで置き換えます。

using System.Diagnostics;

namespace ProcessTasksAsTheyFinish;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

フィールドを追加する

Program クラスの定義に、次の 2 つのフィールドを追加します。

static readonly HttpClient s_client = new HttpClient
{
    MaxResponseContentBufferSize = 1_000_000
};

static readonly IEnumerable<string> s_urlList = new string[]
{
    "https://learn.microsoft.com",
    "https://learn.microsoft.com/aspnet/core",
    "https://learn.microsoft.com/azure",
    "https://learn.microsoft.com/azure/devops",
    "https://learn.microsoft.com/dotnet",
    "https://learn.microsoft.com/dynamics365",
    "https://learn.microsoft.com/education",
    "https://learn.microsoft.com/enterprise-mobility-security",
    "https://learn.microsoft.com/gaming",
    "https://learn.microsoft.com/graph",
    "https://learn.microsoft.com/microsoft-365",
    "https://learn.microsoft.com/office",
    "https://learn.microsoft.com/powershell",
    "https://learn.microsoft.com/sql",
    "https://learn.microsoft.com/surface",
    "https://learn.microsoft.com/system-center",
    "https://learn.microsoft.com/visualstudio",
    "https://learn.microsoft.com/windows",
    "https://learn.microsoft.com/maui"
};

HttpClient では、HTTP 要求を送信して HTTP 応答を受信する機能が公開されます。 s_urlList には、このアプリケーションで処理を計画するすべての URL が格納されます。

アプリケーション エントリ ポイントの更新

コンソール アプリケーションのメイン エントリ ポイントは、Main メソッドです。 既存のメソッドを以下に置き換えます。

static Task Main() => SumPageSizesAsync();

更新した Main メソッドは、async main と見なされるようになります。これにより、実行可能ファイルへの非同期エントリ ポイントが可能になります。 これは SumPageSizesAsync の呼び出しとして表されます。

非同期の合計ページ サイズ メソッドの作成

Main メソッドの下に、SumPageSizesAsync メソッドを追加します。

static async Task SumPageSizesAsync()
{
    var stopwatch = Stopwatch.StartNew();

    IEnumerable<Task<int>> downloadTasksQuery =
        from url in s_urlList
        select ProcessUrlAsync(url, s_client);

    List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

    int total = 0;
    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

    stopwatch.Stop();

    Console.WriteLine($"\nTotal bytes returned:  {total:#,#}");
    Console.WriteLine($"Elapsed time:          {stopwatch.Elapsed}\n");
}

while ループは、各イテレーションでタスクを 1 つ削除します。 すべてのタスクが完了すると、ループは終了します。 このメソッドを開始するには、Stopwatch をインスタンス化して開始します。 それには、実行時にタスクのコレクションを作成するクエリが含まれます。 次のコードの ProcessUrlAsync への各呼び出しは、TResult が整数である Task<TResult> を返します。

IEnumerable<Task<int>> downloadTasksQuery =
    from url in s_urlList
    select ProcessUrlAsync(url, s_client);

LINQ での遅延実行のため、各タスクを開始するには Enumerable.ToList を呼び出します。

List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

while ループでは、コレクション内の各タスクに対して次の手順が実行されます。

  1. WhenAny への呼び出しを待機し、コレクション内で最初にダウンロードが終了したタスクを識別します。

    Task<int> finishedTask = await Task.WhenAny(downloadTasks);
    
  2. コレクションからそのタスクを削除します。

    downloadTasks.Remove(finishedTask);
    
  3. finishedTask への呼び出しから返される、ProcessUrlAsync を待機します。 finishedTask 変数は Task<TResult> が整数である TResult です。 次の例に示すように、タスクは既に完了していますが、ダウンロードした Web サイトの長さの取得を待機します。 タスクが失敗した場合、AggregateException がスローされる Task<TResult>.Result プロパティの読み取りとは異なり、await からは AggregateException に格納されている最初の子の例外がスローされます。

    total += await finishedTask;
    

プロセス メソッドの追加

SumPageSizesAsync メソッドの下に次の ProcessUrlAsync メソッドを追加します。

static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
    byte[] content = await client.GetByteArrayAsync(url);
    Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

    return content.Length;
}

どの URL に対しても、このメソッドにより、提供される client インスタンスが使用され、応答が byte[] として取得されます。 URL と長さがコンソールに出力された後、長さが返されます。

ダウンロードされた長さが常に同じ順序では表示されないことを確認するために、プログラムを複数回実行します。

注意事項

ループで WhenAny を使って、例に示すように、いくつかのタスクを格納する問題を解決できます。 ただし、多数のタスクが処理する場合、他のアプローチがより効率的です。 詳細と例については、「Processing Tasks as they complete」 (完了したタスクを処理する) を参照してください。

コード例全体

次のコードは、この例の Program.cs ファイルの完全なテキストです。

using System.Diagnostics;

HttpClient s_client = new()
{
    MaxResponseContentBufferSize = 1_000_000
};

IEnumerable<string> s_urlList = new string[]
{
    "https://learn.microsoft.com",
    "https://learn.microsoft.com/aspnet/core",
    "https://learn.microsoft.com/azure",
    "https://learn.microsoft.com/azure/devops",
    "https://learn.microsoft.com/dotnet",
    "https://learn.microsoft.com/dynamics365",
    "https://learn.microsoft.com/education",
    "https://learn.microsoft.com/enterprise-mobility-security",
    "https://learn.microsoft.com/gaming",
    "https://learn.microsoft.com/graph",
    "https://learn.microsoft.com/microsoft-365",
    "https://learn.microsoft.com/office",
    "https://learn.microsoft.com/powershell",
    "https://learn.microsoft.com/sql",
    "https://learn.microsoft.com/surface",
    "https://learn.microsoft.com/system-center",
    "https://learn.microsoft.com/visualstudio",
    "https://learn.microsoft.com/windows",
    "https://learn.microsoft.com/maui"
};

await SumPageSizesAsync();

async Task SumPageSizesAsync()
{
    var stopwatch = Stopwatch.StartNew();

    IEnumerable<Task<int>> downloadTasksQuery =
        from url in s_urlList
        select ProcessUrlAsync(url, s_client);

    List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

    int total = 0;
    while (downloadTasks.Any())
    {
        Task<int> finishedTask = await Task.WhenAny(downloadTasks);
        downloadTasks.Remove(finishedTask);
        total += await finishedTask;
    }

    stopwatch.Stop();

    Console.WriteLine($"\nTotal bytes returned:    {total:#,#}");
    Console.WriteLine($"Elapsed time:              {stopwatch.Elapsed}\n");
}

static async Task<int> ProcessUrlAsync(string url, HttpClient client)
{
    byte[] content = await client.GetByteArrayAsync(url);
    Console.WriteLine($"{url,-60} {content.Length,10:#,#}");

    return content.Length;
}

// Example output:
// https://learn.microsoft.com                                      132,517
// https://learn.microsoft.com/powershell                            57,375
// https://learn.microsoft.com/gaming                                33,549
// https://learn.microsoft.com/aspnet/core                           88,714
// https://learn.microsoft.com/surface                               39,840
// https://learn.microsoft.com/enterprise-mobility-security          30,903
// https://learn.microsoft.com/microsoft-365                         67,867
// https://learn.microsoft.com/windows                               26,816
// https://learn.microsoft.com/maui                               57,958
// https://learn.microsoft.com/dotnet                                78,706
// https://learn.microsoft.com/graph                                 48,277
// https://learn.microsoft.com/dynamics365                           49,042
// https://learn.microsoft.com/office                                67,867
// https://learn.microsoft.com/system-center                         42,887
// https://learn.microsoft.com/education                             38,636
// https://learn.microsoft.com/azure                                421,663
// https://learn.microsoft.com/visualstudio                          30,925
// https://learn.microsoft.com/sql                                   54,608
// https://learn.microsoft.com/azure/devops                          86,034

// Total bytes returned:    1,454,184
// Elapsed time:            00:00:01.1290403

関連項目