Reasoning Task, Async & Await in .NET

Bnaya Eshet
Stackademic
Published in
5 min readAug 29, 2023

--

As developers, we often encounter situations where we need to perform tasks concurrently to ensure our applications run efficiently. The asynchronous programming model in .NET brings us the powerful trio: Task, async, and await. In this post, we'll unravel the mysteries behind these concepts and explore how they enable parallel execution, providing a clear understanding of how they work together.

Unraveling the Essence of Task

Before we embark on our journey into the intricacies of async and await, let's take a moment to fathom the core of the Task class. People tend to believe, that a Task isn't merely a conduit for thread initiation or a guaranteed ticket to parallel execution.

Are they right?

Take a look at the following snippet:

Task<byte[]> GetInfoAsync(int id)
{
var key = id.ToString();
if (cache.Contains(key))
{
var res = cache.Get(key) as Task<byte[]>;
return res;
}
var client = new HttpClient();
var result = client.GetByteArrayAsync($"https://randomfox.ca/images/{key}.jpg")
.ContinueWith(t =>
{
if(t.Exception != null)
throw t.Exception;
cache.Add(key, t, DateTimeOffset.UtcNow.AddMinutes(2));
return t.Result;
});
return result;
}

Is it a necessary asynchronous method, or rather we should think of it as a maybe asynchronous method?

If it cached it will return the result right away (won’t await anything).

Tasks should be thought of as a data structure representing an execution state and outcome, which encapsulates the essence of both synchronous and asynchronous execution.

Consider a scenario akin to ordering a tool online. Imagine you urgently need a screwdriver for a quick fix. You place an order, eagerly await its delivery, and, upon receipt, complete your task. Later, when you need the same screwdriver again, you don’t have to reorder and await its delivery anew. Instead, you retrieve it promptly from your toolbox. Similarly, a Task might represent an already completed operation that can be instantly accessed, bypassing the need for further asynchronous orchestration.

Navigating the Async Await Symphony

Having unearthed the true essence of Task, let's now immerse ourselves in the harmonious duet of async and await.

Take a look:

async Task<byte[]> GetInfoAsync(int id)
{
var key = id.ToString();
if (cache.Contains(key))
{
var res = cache.Get(key) as byte[];
return res;
}
var client = new HttpClient();
byte[] result = await client.GetByteArrayAsync($"https://randomfox.ca/images/{key}.jpg");
cache.Add(key, result, DateTimeOffset.UtcNow.AddMinutes(2));
return result;
}

This is the same method we implemented earlier but with enhanced readability. As previously mentioned, this method might execute on the caller’s thread if the data is cached. Alternatively, in the scenario of fetching data from GetByteArrayAsync, the method may progress up to that point and then pause (setting the Thread free), while awaiting the signal of the IO completion Port. Once the signal is received, the subsequent code will be scheduled for execution on a ThreadPool thread (in default).

async doesn't inherently perform complex actions; rather, it serves as a marker, capturing our attention and indicating that this method could potentially await a future result (or it might not).

await signifies the juncture in the code where the possibility of producing a future result emerges. Unlike the Task class, which is a component of a library (essentially, a type), async and await are distinctive compiler features.

The compiler will break the method at each await point and generate optimized code that appropriately schedules the code following the await, or keep it on the calling thread if it is an already completed task (like in the case of a cached result).

Test yourself

Let’s enhance our comprehension by exploring a few simplistic (though not necessarily practical) code snippets:

async Task CanBeTrickyAsync()
{
// which thread execute the first part of the code?
Thread.Sleep(2000);
// which thread execute the rest?
}

Nothing extraordinary unfolds here; this code operates synchronously, causing it to block the caller thread.

This is how the code might appear without utilizing the async feature:

Task CanBeTrickyDemystifyAsync()
{
// which thread execute the first part of the code?
Thread.Sleep(2000);
// which thread execute the rest?
return Task.CompletedTask;
}

Let’s now examine the following snippet:

async Task CanBeTrickyAsync()
{
// which thread execute the first part of the code?
await Task.CompletedTask;
// which thread execute the rest?
}

Similar to the previous example, but without the 2-second block. In this case, the compiler-generated code demonstrates its intelligence by inspecting the status of the Task and recognizing that it's already in a completed state. Consequently, it refrains from scheduling the execution and maintains the continuity of the operation on the calling Thread.

The final sample sets itself apart in the following manner:

async Task CanBeTrickyAsync()
{
// which thread execute the first part of the code?
await Task.Delay(2000);
// which thread execute the rest?
}

The Task.Delay returns a Task that transitions into a completed state at 2 seconds into the future. Consequently, for these 2 seconds, no thread becomes associated with the method. Subsequently, as the Task enters it’s completion state, the remaining code is scheduled for execution on a ThreadPool thread (unless operating within a non-default Task Scheduler context).

Summary

A Task is fundamentally a data structure with the capacity to represent outcomes of both synchronous and asynchronous executions. On the other hand, async and await are compiler features designed to divide the code at await locations and subsequently craft optimized code capable of managing both synchronous and asynchronous scenarios adeptly.

The next time you employ these features, take a moment to reconsider the essence of what you’re truly crafting in your code. Keep in mind the intricate interplay of Task, async, and await, and how they collectively contribute to the efficiency and effectiveness of your software architecture. Happy coding!

Thank you for reading until the end. Please consider following the writer and this publication. Visit Stackademic to find out more about how we are democratizing free programming education around the world.

--

--