次の方法で共有


完了時に非同期タスクを処理する (C#)

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

次の例では、クエリを使用してタスクのコレクションを作成します。 各タスクは、指定された Web サイトの内容をダウンロードします。 while ループの各イテレーションで、 WhenAny の待機中の呼び出しは、最初にダウンロードを完了するタスクのコレクション内のタスクを返します。 そのタスクはコレクションから削除され、処理されます。 ループは、コレクションにこれ以上タスクが含まれるまで繰り返されます。

[前提条件]

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

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

サンプル アプリケーションを作成する

新しい .NET Core コンソール アプリケーションを作成します。 作成するには、 dotnet の新しいコンソール コマンドを使用するか、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 メソッドは 非同期メインと見なされるようになりました。これにより、実行可能ファイルへの非同期エントリ ポイントが可能になります。 これは、 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 を呼び出すたびに、 Task<TResult>が返されます。ここで、 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. ProcessUrlAsyncの呼び出しによって返されるfinishedTaskを待機します。 finishedTask変数は、TResultが整数であるTask<TResult>です。 タスクは既に完了していますが、次の例に示すように、ダウンロードした Web サイトの長さを取得するまで待機します。 タスクに障害が発生した場合、 awaitAggregateExceptionに格納されている最初の子例外をスローします。 Task<TResult>.Result プロパティを読み取ると、 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 をループ内で使用して、少数のタスクに関連する問題を解決できます。 ただし、処理するタスクが多数ある場合は、他の方法の方が効率的です。 詳細と例については、 完了時の処理タスクを参照してください。

を使用してアプローチを簡略化する Task.WhenEach

SumPageSizesAsync メソッドで実装されたwhile ループは、.NET 9 で導入された新しいTask.WhenEach メソッドを使用して、await foreach ループで呼び出すことで簡略化できます。
前に実装した while ループを置き換えます。

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

簡略化された await foreachを使用します。

    await foreach (Task<int> t in Task.WhenEach(downloadTasks))
    {
        total += await t;
    }

この新しいアプローチでは、タスクを手動で呼び出して終了したタスクを削除するために、 Task.WhenAny を繰り返し呼び出す必要がなくなりました。これは、 Task.WhenEach完了の順序でタスクを反復処理するためです。

完全な例

次のコードは、この例の 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

こちらも参照ください