Start Multiple Async Tasks and Process Them As They Complete (C# and Visual Basic)

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.

Note

To run the examples, you must have Visual Studio 2012, Visual Studio 2013, Visual Studio Express 2012 for Windows Desktop, Visual Studio Express 2013 for Windows, or the .NET Framework 4.5 or 4.5.1 installed on your computer.

Downloading the Example

You can download the complete Windows Presentation Foundation (WPF) project from Async Sample: Fine Tuning Your Application and then follow these steps.

  1. Decompress the file that you downloaded, and then start Visual Studio.

  2. On the menu bar, choose File, Open, Project/Solution.

  3. In the Open Project dialog box, open the folder that holds the sample code that you decompressed, and then open the solution (.sln) file for AsyncFineTuningCS or AsyncFineTuningVB.

  4. In Solution Explorer, open the shortcut menu for the ProcessTasksAsTheyFinish project, and then choose Set as StartUp Project.

  5. Choose the F5 key to run the project.

    Choose the Ctrl+F5 keys to run the project without debugging it.

  6. Run the project several times to verify that the downloaded lengths don't always appear in the same order.

If you don't want to download the project, you can review the MainWindow.xaml.vb and MainWindow.xaml.cs files at the end of this topic.

Building the Example

This example adds to the code that’s developed in Cancel Remaining Async Tasks after One Is Complete (C# and Visual Basic) and uses the same UI.

To build the example yourself, step by step, follow the instructions in the "Downloading the Example" section, but choose CancelAfterOneTask as the StartUp Project. Add the changes in this topic to the AccessTheWebAsync method in that project. The changes are marked with asterisks.

The CancelAfterOneTask project already 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.

Dim downloadTasksQuery As IEnumerable(Of Task(Of Integer)) =
    From url In urlList Select ProcessURLAsync(url, client, ct)
IEnumerable<Task<int>> downloadTasksQuery =
    from url in urlList select ProcessURL(url, client, ct);

In the MainWindow.xaml.vb or MainWindow.xaml.cs file of the project, make the following changes to the AccessTheWebAsync method.

  • Execute the query by applying Enumerable.ToList<TSource> instead of ToArray<TSource>.

    Dim downloadTasks As List(Of Task(Of Integer)) = downloadTasksQuery.ToList()
    
    List<Task<int>> downloadTasks = downloadTasksQuery.ToList();
    
  • Add a while loop that performs the following steps for each task in the collection.

    1. Awaits a call to WhenAny to identify the first task in the collection to finish its download.

      Dim firstFinishedTask As Task(Of Integer) = Await Task.WhenAny(downloadTasks)
      
      Task<int> firstFinishedTask = await Task.WhenAny(downloadTasks);
      
    2. Removes that task from the collection.

      downloadTasks.Remove(firstFinishedTask)
      
      downloadTasks.Remove(firstFinishedTask);
      
    3. Awaits firstFinishedTask, which is returned by a call to ProcessURLAsync. The firstFinishedTask variable is a Task<TResult> where TReturn is an integer. The task is already complete, but you await it to retrieve the length of the downloaded website, as the following example shows.

      Dim length = Await firstFinishedTask
      resultsTextBox.Text &= String.Format(vbCrLf & "Length of the downloaded website:  {0}" & vbCrLf, length)
      
      int length = await firstFinishedTask;
      resultsTextBox.Text += String.Format("\r\nLength of the download:  {0}", length);
      

You should run the project several times to verify that the downloaded lengths don't always appear in the same order.

Warning

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.

Complete Example

The following code is the complete text of the MainWindow.xaml.vb or MainWindow.xaml.cs file for the example. Asterisks mark the elements that were added for this example.

Notice that you must add a reference for System.Net.Http.

You can download the project from Async Sample: Fine Tuning Your Application.

' Add an Imports directive and a reference for System.Net.Http. 
Imports System.Net.Http

' Add the following Imports directive for System.Threading. 
Imports System.Threading

Class MainWindow

    ' Declare a System.Threading.CancellationTokenSource. 
    Dim cts As CancellationTokenSource


    Private Async Sub startButton_Click(sender As Object, e As RoutedEventArgs)

        ' Instantiate the CancellationTokenSource.
        cts = New CancellationTokenSource()

        resultsTextBox.Clear()

        Try
            Await AccessTheWebAsync(cts.Token)
            resultsTextBox.Text &= vbCrLf & "Downloads complete." 

        Catch ex As OperationCanceledException
            resultsTextBox.Text &= vbCrLf & "Downloads canceled." & vbCrLf

        Catch ex As Exception
            resultsTextBox.Text &= vbCrLf & "Downloads failed." & vbCrLf
        End Try 

        ' Set the CancellationTokenSource to Nothing when the download is complete.
        cts = Nothing 
    End Sub 


    ' You can still include a Cancel button if you want to. 
    Private Sub cancelButton_Click(sender As Object, e As RoutedEventArgs)

        If cts IsNot Nothing Then
            cts.Cancel()
        End If 
    End Sub 


    ' Provide a parameter for the CancellationToken. 
    ' Change the return type to Task because the method has no return statement.
    Async Function AccessTheWebAsync(ct As CancellationToken) As Task

        Dim client As HttpClient = New HttpClient()

        ' Call SetUpURLList to make a list of web addresses. 
        Dim urlList As List(Of String) = SetUpURLList()

        ' ***Create a query that, when executed, returns a collection of tasks. 
        Dim downloadTasksQuery As IEnumerable(Of Task(Of Integer)) =
            From url In urlList Select ProcessURLAsync(url, client, ct)

        ' ***Use ToList to execute the query and start the download tasks.  
        Dim downloadTasks As List(Of Task(Of Integer)) = downloadTasksQuery.ToList()

        ' ***Add a loop to process the tasks one at a time until none remain. 
        While downloadTasks.Count > 0
            ' ***Identify the first task that completes. 
            Dim firstFinishedTask As Task(Of Integer) = Await Task.WhenAny(downloadTasks)

            ' ***Remove the selected task from the list so that you don't 
            ' process it more than once.
            downloadTasks.Remove(firstFinishedTask)

            ' ***Await the first completed task and display the results. 
            Dim length = Await firstFinishedTask
            resultsTextBox.Text &= String.Format(vbCrLf & "Length of the downloaded website:  {0}" & vbCrLf, length)
        End While 

    End Function 


    ' Bundle the processing steps for a website into one async method.
    Async Function ProcessURLAsync(url As String, client As HttpClient, ct As CancellationToken) As Task(Of Integer)

        ' GetAsync returns a Task(Of HttpResponseMessage).  
        Dim response As HttpResponseMessage = Await client.GetAsync(url, ct)

        ' Retrieve the website contents from the HttpResponseMessage. 
        Dim urlContents As Byte() = Await response.Content.ReadAsByteArrayAsync()

        Return urlContents.Length
    End Function 


    ' Add a method that creates a list of web addresses. 
    Private Function SetUpURLList() As List(Of String)

        Dim urls = New List(Of String) From
            {
                "https://msdn.microsoft.com",
                "https://msdn.microsoft.com/en-us/library/hh290138.aspx",
                "https://msdn.microsoft.com/en-us/library/hh290140.aspx",
                "https://msdn.microsoft.com/en-us/library/dd470362.aspx",
                "https://msdn.microsoft.com/en-us/library/aa578028.aspx",
                "https://msdn.microsoft.com/en-us/library/ms404677.aspx",
                "https://msdn.microsoft.com/en-us/library/ff730837.aspx"
            }
        Return urls
    End Function 

End Class 


' Sample output: 

' Length of the download:  226093 
' Length of the download:  412588 
' Length of the download:  175490 
' Length of the download:  204890 
' Length of the download:  158855 
' Length of the download:  145790 
' Length of the download:  44908 
' Downloads complete.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

// Add a using directive and a reference for System.Net.Http. 
using System.Net.Http;

// Add the following using directive. 
using System.Threading;


namespace ProcessTasksAsTheyFinish
{
    public partial class MainWindow : Window
    {
        // Declare a System.Threading.CancellationTokenSource.
        CancellationTokenSource cts;

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void startButton_Click(object sender, RoutedEventArgs e)
        {
            resultsTextBox.Clear();

            // Instantiate the CancellationTokenSource.
            cts = new CancellationTokenSource();

            try
            {
                await AccessTheWebAsync(cts.Token);
                resultsTextBox.Text += "\r\nDownloads complete.";
            }
            catch (OperationCanceledException)
            {
                resultsTextBox.Text += "\r\nDownloads canceled.\r\n";
            }
            catch (Exception)
            {
                resultsTextBox.Text += "\r\nDownloads failed.\r\n";
            }

            cts = null;
        }


        private void cancelButton_Click(object sender, RoutedEventArgs e)
        {
            if (cts != null)
            {
                cts.Cancel();
            }
        }


        async Task AccessTheWebAsync(CancellationToken ct)
        {
            HttpClient client = new HttpClient();

            // Make a list of web addresses.
            List<string> urlList = SetUpURLList();

            // ***Create a query that, when executed, returns a collection of tasks.
            IEnumerable<Task<int>> downloadTasksQuery =
                from url in urlList select ProcessURL(url, client, ct);

            // ***Use ToList to execute the query and start the tasks. 
            List<Task<int>> downloadTasks = downloadTasksQuery.ToList();

            // ***Add a loop to process the tasks one at a time until none remain. 
            while (downloadTasks.Count > 0)
            {
                    // Identify the first task that completes.
                    Task<int> firstFinishedTask = await Task.WhenAny(downloadTasks);

                    // ***Remove the selected task from the list so that you don't 
                    // process it more than once.
                    downloadTasks.Remove(firstFinishedTask);

                    // Await the completed task. 
                    int length = await firstFinishedTask;
                    resultsTextBox.Text += String.Format("\r\nLength of the download:  {0}", length);
            }
        }


        private List<string> SetUpURLList()
        {
            List<string> urls = new List<string> 
            { 
                "https://msdn.microsoft.com",
                "https://msdn.microsoft.com/library/windows/apps/br211380.aspx",
                "https://msdn.microsoft.com/en-us/library/hh290136.aspx",
                "https://msdn.microsoft.com/en-us/library/dd470362.aspx",
                "https://msdn.microsoft.com/en-us/library/aa578028.aspx",
                "https://msdn.microsoft.com/en-us/library/ms404677.aspx",
                "https://msdn.microsoft.com/en-us/library/ff730837.aspx"
            };
            return urls;
        }


        async Task<int> ProcessURL(string url, HttpClient client, CancellationToken ct)
        {
            // GetAsync returns a Task<HttpResponseMessage>. 
            HttpResponseMessage response = await client.GetAsync(url, ct);

            // Retrieve the website contents from the HttpResponseMessage. 
            byte[] urlContents = await response.Content.ReadAsByteArrayAsync();

            return urlContents.Length;
        }
    }
}

// Sample Output: 

// Length of the download:  226093 
// Length of the download:  412588 
// Length of the download:  175490 
// Length of the download:  204890 
// Length of the download:  158855 
// Length of the download:  145790 
// Length of the download:  44908 
// Downloads complete.

See Also

Reference

WhenAny

Concepts

Fine-Tuning Your Async Application (C# and Visual Basic)

Asynchronous Programming with Async and Await (C# and Visual Basic)

Other Resources

Async Sample: Fine Tuning Your Application