完了時の非同期タスクの処理 (C#)
Task.WhenAny を使用すると、複数のタスクを、開始された順番に処理するのでなく、同時に開始して完了するごとに 1 つずつ処理できます。
クエリを使用して、タスクのコレクションを作成する例を次に示します。 各タスクは、指定された Web サイトのコンテンツをダウンロードします。 while ループの各反復で、待機されている WhenAny への呼び出しは、最初にダウンロードを終了するタスクのコレクションにあるタスクを返します。 タスクはコレクションから削除され、処理されます。 ループは、コレクションのタスクがなくなるまで繰り返されます。
前提条件
次のいずれかのオプションを使用してこのチュートリアルを進めることができます。
- .NET デスクトップ開発ワークロードをインストールした Visual Studio 2022。 このワークロードを選択すると、.NET SDK が自動的にインストールされます。
- .NET SDK と Visual 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
ループでは、コレクション内の各タスクに対して次の手順が実行されます。
WhenAny
への呼び出しを待機し、コレクション内で最初にダウンロードが終了したタスクを識別します。Task<int> finishedTask = await Task.WhenAny(downloadTasks);
コレクションからそのタスクを削除します。
downloadTasks.Remove(finishedTask);
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
関連項目
.NET