C# Await Multiple Tasks

Naila Saad Siddiqui Oct 12, 2023
  1. Synchronous and Asynchronous Programming in C#
  2. Implement async Method and await Multiple Tasks in C#
C# Await Multiple Tasks

This trivial guide discusses the asynchronous programming model and explains the related keywords like async, await, and Task. It particularly demonstrates the concept of awaiting multiple tasks using C#.

Before moving toward the actual topic, we must briefly overview the differences between synchronous and asynchronous programming models. So, let’s get started!

Synchronous and Asynchronous Programming in C#

Synchronous and asynchronous models are crucial in computer programming. The terminologies provide information about the functions of each programming model and their distinctions.

Synchronous tasks are usually carried out in sequential order, one after the other. Asynchronous tasks can be carried out concurrently (in any order).

Synchronous Programming in C#

Synchronous is a blocking architecture that is best suited for reactive programming systems. As a single-thread model, it adheres to a rigid set of sequences, so each operation is carried out one at a time and in precise succession.

Instructions for further procedures are blocked while one process is being carried out. The completion of one task starts the next, and so forth.

Consider a walkie-talkie example to understand how synchronous programming operates.

One person speaks during a phone call while the other listens. The second person typically responds immediately after the first person has finished talking.

In terms of programming, let’s consider the following example to understand synchronous programming:

using System;
using System.Threading;
public class Program {
  public static void Main(string[] args) {
    Process1();
    Process2();
  }
  static void Process1() {
    Console.WriteLine("Process 1 started");
    // You can write some code here that can take a longer time
    Thread.Sleep(5000);  // holing on execution for 5 sec
    Console.WriteLine("Process 1 Completed");
  }
  static void Process2() {
    Console.WriteLine("Process 2 Started");
    // write some code here that takes relatively less time
    Console.WriteLine("Process 2 Completed");
  }
}

The Process1() method in the example above is used to do a lengthy process, such as reading a file from the server, contacting a web API that returns a lot of data, or uploading or downloading a huge file.

It takes a little longer to run (for illustration purposes, Thread.Sleep(5000) holds it for 5 seconds). The process2() method follows the Process1() method and does not start concurrently.

The program above runs synchronously. It indicates that execution begins with the Main() function, after which the Process1() method and the Process2() method are each carried out in order (i.e., strictly one following the other).

An application is halted and rendered unresponsive during execution (see this in the output screen below). Synchronous programming is waiting to proceed to the following line until the previous line has finished running.

Let’s look at the output to understand synchronous programming completely.

Synchronous Programming output

You can see from the output above that the execution is blocked while waiting for Process1 to complete. As soon as its execution is completed, Process2 executes.

Asynchronous Programming in C#

On the other hand, asynchronous programming is a multithreaded style that works best in networking and communications. As a non-blocking design, asynchronous architecture doesn’t prevent subsequent execution while one or more operations are running.

Multiple related operations can execute simultaneously with asynchronous programming without waiting for other actions to finish. Instead of replying immediately after receiving a message, persons engaged in asynchronous communication wait until it is convenient or feasible before reading and processing it.

Asynchronous communication techniques include texting. One person can send a text message, and the recipient can reply whenever they want. While waiting for a response, the sender may do other actions.

According to the example discussed above, the Process1() method, for instance, will be executed in a different thread from the thread pool in the asynchronous programming model. In contrast, the main application thread will continue to execute the subsequent statement.

Microsoft suggests using the Task-based Asynchronous Pattern when implementing asynchronous programming in .NET Framework or .NET Core applications using the async and await keywords and the Task or Task<TResult> class.

Let’s rewrite the previous code using asynchronous programming:

using System;
using System.Threading.Tasks;

public class Program {
  public static async Task Main(string[] args) {
    Process1();
    Process2();
    Console.ReadKey();
  }

  public static async void Process1() {
    Console.WriteLine("Process 1 Started");
    await Task.Delay(5000);  // holding execution for 5 seconds
    Console.WriteLine("Process 1 Completed");
  }
  static void Process2() {
    Console.WriteLine("Process 2 Started");
    // Write code here
    Console.WriteLine("Process 2 Completed");
  }
}

The async keyword is used to identify the Main() method in the example above, and Task is the return type.

The method is identified as asynchronous by the async keyword. Remember that every method in the method chain must be async to perform asynchronous programming.

Therefore, to make child methods asynchronous, the Main() function must be async.

The async keyword marks the Process1() method as being asynchronous and waiting for Task.Delay(5000); prevents the thread from executing some useful for 5 seconds.

The async Main() method of the main application thread now initiates the program’s execution. The main application thread continues running the subsequent statement, which uses the Process2() method without waiting for the async Process1() to finish.

The output of this code segment will be:

Asynchronous programming output

Use of the async, await, and Task Keywords

If the async method returns a value to the calling code, use async along with await and Task. In the example above, we used the async keyword to show how to use a basic asynchronous void method.

Before the async method returns a value, the await keyword waits for it. As a result, until it receives a return value, the main application thread hangs there.

The Task class represents an asynchronous operation, and a process that can return a result is represented by the generic type Task<TResult>. In the case above, await Task was used. await retains a thread for 5 seconds after Delay(5000) starts an async operation that sleeps for 5 seconds.

The async method returns a value in the example below.

Implement async Method and await Multiple Tasks in C#

Consider an example in which we have two processes, each returns a value, and those values are passed to some other function that computes the sum of these two (Sum is calculated for illustrative purposes, you can perform any operation with the results).

For this purpose, we must wait for the processes to complete and return the value and then pass those returned values to the other function. Thereby, the WhenAll method is used.

The WhenAll method waits for all the processes to complete and then saves the returned results. This is demonstrated in the example below:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public class Program {
  static async Task Main(string[] args) {
    Task<int> res1 = Process1();
    Task<int> res2 = Process2();

    Console.WriteLine("After two processes.");
    var ls = await Task.WhenAll(res1, res2);
    DoSomeTask(ls);

    Console.ReadKey();
  }

  static async Task<int> Process1() {
    Console.WriteLine("Process 1 Started");
    await Task.Delay(5000);  // hold execution for 5 seconds
    Console.WriteLine("Process 1 Completed");
    return 15;
  }

  static async Task<int> Process2() {
    Console.WriteLine("Process 2 Started");
    await Task.Delay(3000);  // hold execution for 3 seconds
    Console.WriteLine("Process 2 Completed");
    return 25;
  }

  static void DoSomeTask(int[] l) {
    int sum = 0;
    foreach (int i in l) sum += i;
    Console.WriteLine("sum of the list is: {0} ", sum);
  }
}

In the code segment above, we have created two functions, Process1 and Process2. Both of these functions have a delay of 5 seconds and 3 seconds.

When the two processes are completed, we pass the returned values to DoSomeTask, where we calculate the sum of these numbers and display it on the screen. The output of this code segment will be:

Asynchronous programming with return values