In this article, we will discuss the differences between await and Task.Wait() in C# and .NET. We will start by looking into concepts like Blocking Code and Asynchronous Programming. We will see how we can use await and Task.Wait() in our code and their effects on our applications. Finally, we will suggest which one we should be using and why.
So, let’s start.
Why Blocking Code Is Bad
We write code to build applications. Our applications are sets of commands hosted in OS processes. More precisely, each process utilizes a limited number of threads, managed by a thread pool, to execute the various commands of our applications.
Every command in our software executes in a thread:
As mentioned, there is a limit on the maximum number of threads that a process can have. Therefore, we should ensure that we utilize threads efficiently. That is, our code should only reserve a thread for as little as possible and free up threads, which means returning them to the thread pool, as soon as they are no longer required.
However, there are commands that by nature will require much time to complete.
There are 2 categories of such long operations:
- CPU-intensive: Heavy operations that stress out the CPU and require significant time to complete. Examples of such operations are complex mathematical calculations and video or image compression.
- I/O-intensive: Operations that heavily rely on external and remote resources and involve significant wait times. Examples include writing to/reading from the database and calling Web APIs.
Blocking code is code that reserves a thread for the whole period until a CPU or I/O intensive operation completes. This is often a long period and such code often leads to inefficiencies, unresponsiveness, and bad user experience.
As developers, we should avoid writing blocking code. And we have the tools to achieve that.
Understanding Asynchronous Programming for await and Task.Wait
Asynchronous Programming is a programming paradigm that we can use to avoid blocking threads in our application. The execution of long-running CPU and I/O intensive operations moves to a separate flow from the main execution flow of our application. The threads executing those long-running operations do not block but, they are freed up and used in other tasks until the long-running operation is completed. At that point, code execution resumes with the next operation. This way our system remains responsive and the overall perception is that our application runs faster.
In the latest .NET versions, asynchronous programming is achieved via Task-based Asynchronous Pattern, and the keywords await
and async
.
Now that we have learned the fundamentals of blocking and asynchronous code, let’s continue by seeing the effects that Task.Wait()
and await
have in our applications and how these relate to the above concepts.
Asynchronous Programming and Task.Wait
Task
is a class in the System.Threading.Tasks
namespace that represents an asynchronous operation. When we want to run some operation asynchronously then we can wrap it in a Task
object and start it. Task exposes lots of useful functionality through its interface that allows us to interact with it and the running operation. One of the methods that we can use on Task
objects is Task.Wait()
.
When we call Task.Wait()
we instruct the runtime to stop and wait for the Task to complete its execution, causing the execution to block. That means on stopping, the running thread is not freed up. Rather it stays occupied, waiting for the operation to complete. This also means that the thread running our code before calling Wait()
will also run our code after the call. We understand that calling Task.Wait()
has all the negatives that we mentioned earlier in the article about blocking code.
Let’s see how we can call the Task.Wait()
method on a running Task
:
public static void BlockingCodeExample() { Console.WriteLine("** Blocking code Example **"); Console.WriteLine($"Current ManagedThreadId: {Environment.CurrentManagedThreadId}"); Task taskToBlock = Task.Run(() => { Thread.Sleep(5000); return "Hello, from a blocking task"; }); taskToBlock.Wait(); string blockingResult = taskToBlock.Result; Console.WriteLine($"Current ManagedThreadId: {Environment.CurrentManagedThreadId}"); }
Here, we create the BlockingCodeExample()
method that initially prints, in the console, the CurrentManagedThreadId
of the executing thread. Then, we define an object of type Task
, which wraps a Func
delegate method. The delegate method calls Thread.Sleep()
to mock a long-running operation that takes 5 seconds to complete and returns a string
.
Then, we call the Wait()
method on the Task
that causes the executing thread to block while waiting for the completion of the long-running operation. After that, we call the Result
property of taskToBlock
to retrieve the result of the task.
Finally, we print again the CurrentManagedThreadId
, noticing that the thread is the same.
Let’s check the console output:
** Blocking code Example ** Current ManagedThreadId: 1 Current ManagedThreadId: 1
Let’s continue with the await
operator to see how it differs from what we’ve seen so far.
Asynchronous Programming and await
The await
operator is used to wait for an operation to complete asynchronously. As soon as the runtime notices that a Task
is awaited, execution stops and the running thread is freed up until the asynchronous operation completes. Until that happens, the runtime can use the thread in other operations and the system remains responsive.
Additionally, the await
keyword also “unwraps” the returned value of the asynchronous operation upon its completion and lets us assign it to a variable.
The await
operator can only be used on asynchronous methods marked with the async
operator. It is also a best practice the method to have a return type of Task
or Task<T>
. This way the caller of the method can also use await
. Event handlers are good to be marked async
with a void
return type.
Let’s now see how we can use the await
keyword in an asynchronous method:
public async static Task AsynchronousCodeExampleAsync() { Console.WriteLine("** Asynchronous code Example **"); Console.WriteLine($"Current ManagedThreadId: {Environment.CurrentManagedThreadId}"); Task taskToAwait = Task.Run(() => { Thread.Sleep(5000); return "Hello, from an asyncrhonous task"; }); string awaitResult = await taskToAwait; Console.WriteLine($"Current ManagedThreadId: {Environment.CurrentManagedThreadId}"); }
Here, we declare the AsynchronousCodeExample()
method marking it async
so that we can use the await
operator inside it. We print the CurrentManagedThreadId
of the executing thread and we define the Task
that we will await, mocking a long-running operation which we do by calling Thread.Sleep()
.
Then we use the await
keyword to asynchronously wait for the completion of the Task and to receive its result. Notice that in this case, we don’t need to use the Result
property as we did before. Finally, we print again the CurrentManagedThreadId
.
Let’s see the output of our method:
** Asynchronous code Example ** Current ManagedThreadId: 1 Current ManagedThreadId: 10
Notice that in this case, CurrentManagedThreadId
has changed value after calling the await
keyword. This is because the runtime decides to switch the executing thread. As mentioned earlier, the initial thread is freed up instead of blocking while waiting for completion. It is important to note that depending on the application type and our code, the runtime might wait for the initial thread to be free again to continue execution after the completion of the long-running operation.
Exception Handling With await and Task.Wait
Now that we’ve discussed the differences between await
and Task.Wait()
in the execution flow, let’s also see how they differ in the way that they handle exceptions.
As we’ve seen the await
keyword returns the result of an asynchronous operation on its completion. In addition to that, it also checks if any Exception
has been raised within the asynchronous operation and throws the Exception
back to the main thread. Therefore, the experience that we get is very similar to the one of synchronous execution.
Let’s now see this in action:
public async static Task AsynchronousExceptionHandlingAsync() { Console.WriteLine("** Asynchronous Exception Handling **"); try { Task taskToFail = Task.Run(() => { throw new ApplicationException("Error in the asynchronous long-running operation"); }); await taskToFail; } catch (ApplicationException ex) { Console.WriteLine(ex.Message); } }
Here, we define the AsynchronousExceptionHandling()
method which runs a Task
that raises an ApplicationException
. We use a try-catch block to wrap the code and handle any exceptions.
Now, let’s run this code and we should see that we successfully catch the ApplicationException
:
** Asynchronous Exception Handling ** Error in the asynchronous long-running operation
Let’s now see how the same code behaves when we use Task.Wait()
:
public static void BlockingExceptionHandling() { Console.WriteLine("** Blocking Exception Handling **"); try { Task taskToFail = Task.Run(() => { throw new ApplicationException("Error in the blocking long-running operation"); }); taskToFail.Wait(); } catch (ApplicationException ex) { Console.WriteLine(ex.Message); } }
Here, we define the BlockingExceptionHandling()
method which runs a Task
that raises an ApplicationException
. The only difference this time is that we don’t await
to the result, instead calling the Wait()
method.
We run this code but we notice that it crashes!
Let’s see the output:
It looks like our code throws a System.AggregateException
instead of the ApplicationException
that we expected. Task-based programming uses the AggregateException to consolidate multiple exceptions that parallel tasks might throw. It is a powerful concept when we deal with concurrent Tasks and we want to check their status and report their failures. However, when handling errors in a single asynchronous task, AggregateException
adds complexity to our code without any benefits.
Conclusion
In this article, we’ve analyzed the differences between the await operator with the Task.Wait() method. We started by discussing what is blocking code, why is it a bad practice, and how we can use asynchronous code instead. We’ve seen how we can use await and Task.Wait() in our code and how they affect the execution flow of our programs. We’ve seen how await makes our Exception handling easier and why we should use it to build efficient and responsive systems.