Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
By using Task.WhenAny, you can start multiple tasks at the same time and process them one by one as they're completed rather than process them in the order in which they're started.
The following example uses a query to create a collection of tasks. Each task downloads the contents of a specified website. In each iteration of a while loop, an awaited call to WhenAny returns the task in the collection of tasks that finishes its download first. That task is removed from the collection and processed. The loop repeats until the collection contains no more tasks.
Prerequisites
You can follow this tutorial by using one of the following options:
- Visual Studio 2022 with the .NET desktop development workload installed. The .NET SDK is automatically installed when you select this workload.
- The .NET SDK with a code editor of your choice, such as Visual Studio Code.
Create example application
Create a new .NET Core console application. You can create one by using the dotnet new console command or from Visual Studio.
Open the Program.cs file in your code editor, and replace the existing code with this code:
using System.Diagnostics;
namespace ProcessTasksAsTheyFinish;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
Add fields
In the Program class definition, add the following two fields:
static readonly HttpClient s_client = new HttpClient
{
MaxResponseContentBufferSize = 1_000_000
};
static readonly IEnumerable<string> s_urlList = new string[]
{
"https://free.blessedness.top",
"https://free.blessedness.top/aspnet/core",
"https://free.blessedness.top/azure",
"https://free.blessedness.top/azure/devops",
"https://free.blessedness.top/dotnet",
"https://free.blessedness.top/dynamics365",
"https://free.blessedness.top/education",
"https://free.blessedness.top/enterprise-mobility-security",
"https://free.blessedness.top/gaming",
"https://free.blessedness.top/graph",
"https://free.blessedness.top/microsoft-365",
"https://free.blessedness.top/office",
"https://free.blessedness.top/powershell",
"https://free.blessedness.top/sql",
"https://free.blessedness.top/surface",
"https://free.blessedness.top/system-center",
"https://free.blessedness.top/visualstudio",
"https://free.blessedness.top/windows",
"https://free.blessedness.top/maui"
};
The HttpClient exposes the ability to send HTTP requests and receive HTTP responses. The s_urlList holds all of the URLs that the application plans to process.
Update application entry point
The main entry point into the console application is the Main method. Replace the existing method with the following:
static Task Main() => SumPageSizesAsync();
The updated Main method is now considered an Async main, which allows for an asynchronous entry point into the executable. It is expressed as a call to SumPageSizesAsync.
Create the asynchronous sum page sizes method
Below the Main method, add the SumPageSizesAsync method:
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");
}
The while loop removes one of the tasks in each iteration. After every task has completed, the loop ends. The method starts by instantiating and starting a Stopwatch. It then includes a query that, when executed, creates a collection of tasks. Each call to ProcessUrlAsync in the following code returns a Task<TResult>, where TResult is an integer:
IEnumerable<Task<int>> downloadTasksQuery =
from url in s_urlList
select ProcessUrlAsync(url, s_client);
Due to deferred execution with the LINQ, you call Enumerable.ToList to start each task.
List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
The while loop performs the following steps for each task in the collection:
Awaits a call to
WhenAnyto identify the first task in the collection that has finished its download.Task<int> finishedTask = await Task.WhenAny(downloadTasks);Removes that task from the collection.
downloadTasks.Remove(finishedTask);Awaits
finishedTask, which is returned by a call toProcessUrlAsync. ThefinishedTaskvariable is a Task<TResult> whereTResultis an integer. The task is already complete, but you await it to retrieve the length of the downloaded website, as the following example shows. If the task is faulted,awaitwill throw the first child exception stored in theAggregateException, unlike reading the Task<TResult>.Result property, which would throw theAggregateException.total += await finishedTask;
Add process method
Add the following ProcessUrlAsync method below the SumPageSizesAsync method:
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;
}
For any given URL, the method will use the client instance provided to get the response as a byte[]. The length is returned after the URL and length is written to the console.
Run the program several times to verify that the downloaded lengths don't always appear in the same order.
Caution
You can use WhenAny in a loop, as described in the example, to solve problems that involve a small number of tasks. However, other approaches are more efficient if you have a large number of tasks to process. For more information and examples, see Processing tasks as they complete.
Simplify the approach using Task.WhenEach
The while loop implemented in SumPageSizesAsync method can be simplified using the new Task.WhenEach method introduced in .NET 9, by calling it in await foreach loop.
Replace the previously implemented while loop:
while (downloadTasks.Any())
{
Task<int> finishedTask = await Task.WhenAny(downloadTasks);
downloadTasks.Remove(finishedTask);
total += await finishedTask;
}
with the simplified await foreach:
await foreach (Task<int> t in Task.WhenEach(downloadTasks))
{
total += await t;
}
This new approach allows to no longer repeatedly call Task.WhenAny to manually call a task and remove the one that finishes, because Task.WhenEach iterates through task in an order of their completion.
Complete example
The following code is the complete text of the Program.cs file for the example.
using System.Diagnostics;
HttpClient s_client = new()
{
MaxResponseContentBufferSize = 1_000_000
};
IEnumerable<string> s_urlList = new string[]
{
"https://free.blessedness.top",
"https://free.blessedness.top/aspnet/core",
"https://free.blessedness.top/azure",
"https://free.blessedness.top/azure/devops",
"https://free.blessedness.top/dotnet",
"https://free.blessedness.top/dynamics365",
"https://free.blessedness.top/education",
"https://free.blessedness.top/enterprise-mobility-security",
"https://free.blessedness.top/gaming",
"https://free.blessedness.top/graph",
"https://free.blessedness.top/microsoft-365",
"https://free.blessedness.top/office",
"https://free.blessedness.top/powershell",
"https://free.blessedness.top/sql",
"https://free.blessedness.top/surface",
"https://free.blessedness.top/system-center",
"https://free.blessedness.top/visualstudio",
"https://free.blessedness.top/windows",
"https://free.blessedness.top/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://free.blessedness.top 132,517
// https://free.blessedness.top/powershell 57,375
// https://free.blessedness.top/gaming 33,549
// https://free.blessedness.top/aspnet/core 88,714
// https://free.blessedness.top/surface 39,840
// https://free.blessedness.top/enterprise-mobility-security 30,903
// https://free.blessedness.top/microsoft-365 67,867
// https://free.blessedness.top/windows 26,816
// https://free.blessedness.top/maui 57,958
// https://free.blessedness.top/dotnet 78,706
// https://free.blessedness.top/graph 48,277
// https://free.blessedness.top/dynamics365 49,042
// https://free.blessedness.top/office 67,867
// https://free.blessedness.top/system-center 42,887
// https://free.blessedness.top/education 38,636
// https://free.blessedness.top/azure 421,663
// https://free.blessedness.top/visualstudio 30,925
// https://free.blessedness.top/sql 54,608
// https://free.blessedness.top/azure/devops 86,034
// Total bytes returned: 1,454,184
// Elapsed time: 00:00:01.1290403