DEV Community

Cover image for C# Async Await, Simply
Henrick Tissink
Henrick Tissink

Posted on • Edited on

C# Async Await, Simply

One at a time

Code is executed to achieve a purpose. Usually it's run as a series of steps, eventually leading to a final result. Each of the steps take some time to execute, and running them synchronously, one after the other, can take a significant amount of time.

alt text

Say you're a Doctor of Evil - a Dr. Evil if you will. And you want to run a successful criminal enterprise to take over the world; maybe you're looking to make One Million Dollars... Would it be the most effective way to run all of your plans one after the other?

Running an Evil Empire

alt text

The first thing you'll need, as an evil mastermind, is of course an Evil Lair.

  • Task.Delay(x) gives a logical delay of x milliseconds without blocking the current thread


public static void BuildAnEvilLair()
{
    await Task.Delay(5000);
    Console.WriteLine("Evil Lair Built!");
}


Enter fullscreen mode Exit fullscreen mode

After you build your lair, you're going to need to hire some henchman.



public static void HireSomeHenchman()
{
    await Task.Delay(2000);
    Console.WriteLine("Henchman Hired!");
}


Enter fullscreen mode Exit fullscreen mode

And of course, you'll need to create a mini clone of yourself.



public static void MakeMiniClone()
{
    await Task.Delay(3000);
    Console.WriteLine("Mini Clone Created!");
}


Enter fullscreen mode Exit fullscreen mode

Once all of this is in place, you can start working on your Super Evil Plan!



public static void SuperEvilPlan()
{
    await Task.Delay(4000);
    Console.WriteLine("Super Evil Plan Completed!");
}


Enter fullscreen mode Exit fullscreen mode

And let's not forget threatening world leaders with your Super Evil Plan and ransoming One Million Dollars.

alt text



public static int MakeOneMillionDollars()
{

    await Task.Delay(2000);
    Console.WriteLine("Million Dollars Made!");
    return 1000000;
}


Enter fullscreen mode Exit fullscreen mode

Finally, when everything has been completed, we can achieve World Domination.



public static void WorldDomination()
{
    await Task.Delay(6000);
    Console.WriteLine("World Domination Achieved!");
}


Enter fullscreen mode Exit fullscreen mode

Putting our plan together,



BuildAnEvilLair();
HireSomeHenchman();
MakeMiniClone();
SuperEvilPlan();
var money = MakeOneMillionDollars();
WorldDomination();


Enter fullscreen mode Exit fullscreen mode

which takes 22 000ms (or 22 seconds) synchronously.

All at once

alt text

Many of the steps in such a synchronous plan can be executed in parallel, asynchronously. So how is this done?

First, all the methods will be made asynchronous with the async descriptor, and their executing code will be awaited.

Awaiting asynchronous code, the awaiting code is processed asynchronously, and the code following the await is added as a continuation to the await. The continuation will be run after the code being awaited has run its duration and been successfully executed.



public static async Task BuildAnEvilLair()
{
    await Task.Delay(5000);
    Console.WriteLine("Evil Lair Built!");
}

public static async Task HireSomeHenchman()
{
    await Task.Delay(2000);
    Console.WriteLine("Henchman Hired!");
}

public static async Task MakeMiniClone()
{
    await Task.Delay(3000);
    Console.WriteLine("Mini Clone Created!");
}

public static async Task SuperEvilPlan()
{
    await Task.Delay(4000);
    Console.WriteLine("Super Evil Plan Completed!");
}

public static async Task<int> MakeOneMillionDollars()
{
    await Task.Delay(2000);
    Console.WriteLine("Million Dollars Made!");
    return 1000000;
}

public static async Task WorldDomination()
{
    await Task.Delay(6000);
    Console.WriteLine("World Domination Achieved!");
}


Enter fullscreen mode Exit fullscreen mode

With output

Million Dollars Made!
Super Evil Plan Completed!
Mini Clone Created!
Evil Lair Built!
Henchman Hired!
World Domination Achieved!

alt text

Clearly this is a mess. Everything is happening at once. It's really fast - only dependent on the slowest method (6000ms). It's obvious that some calls are dependent on others. Now we need to synchronise them with each other.

Effectively Asynchronous

So, as an Evil Mastermind, which tasks would you run and in which order?

First, we need an evil lair. After that we can hire some henchman and create a mini clone of ourselves. Then we can come up with a Super Evil Plan, after which we can make One Million Dollars and achieve World Domination.

Changing the executing code we can achieve this by using the right mix of await and Task.WaitAll. Task.WaitAll fires off all the tasks asynchronously and finishes awaiting those tasks once all the tasks have completed - so the Task.WaitAll takes as long as the longest task it's awaiting.

Let's try this again.



// First we need an Evil Lair
await BuildAnEvilLair();

// Next, hire Henchman and create a Clone (asynchronously)
Task.WaitAll(HireSomeHenchman(), MakeMiniClone());

// Now, come up with the evil plan
await SuperEvilPlan();

// And finally, make One Million Dollars (and achieve World Domination)
Task.WaitAll(MakeOneMillionDollars(), WorldDomination());


Enter fullscreen mode Exit fullscreen mode

With output,

Evil Lair Built!
Henchman Hired!
Mini Clone Created!
Super Evil Plan Completed!
Million Dollars Made!
World Domination Achieved!

Taking a cool 18 000ms (18 seconds). 4 seconds less than the synchronous implementation.

So remember, next time you want to become an Evil Genius consider asynchronous execution and some time. If it's up to those darn super spies, every second counts!

Top comments (31)

Collapse
 
glsolaria profile image
G.L Solaria

I understand why you are using Thread.Sleep (to mimic doing some CPU intensive work) but I fear it will also give beginners the wrong impression about using Thread.Sleep in tasks to delay work. I think instead of ...

public static async Task WorldDomination()
{
    await Task.Run(() =>
    {
        Thread.Sleep(6000);
        Console.WriteLine("World Domination Achieved!");
    });
}
Enter fullscreen mode Exit fullscreen mode

If you need to delay before doing something in a task ...

public static async Task WorldDomination()
{
    await Task.Delay(6000);
    Console.WriteLine("World Domination Achieved!");
}
Enter fullscreen mode Exit fullscreen mode

If you Thread.Sleep in many running tasks, performance may suffer. If you do it to excess, work may have to be queued waiting for all the sleeping threads to wake and finish executing their work. At the very worst you could get out of memory exceptions.

Like I said I understand why you chose to use Thread.Sleep for your example but beginners shouldn't think that using Thread.Sleep in tasks is good practice.

Helping others understand async/await is no mean feat. Good on you for giving it a go.

Collapse
 
htissink profile image
Henrick Tissink

You make a good point. Awaiting a Task.Delay is much better - Thread.Sleep was just used for the illustration. I'll fix it when I get the chance :)

Collapse
 
ronaldjenkins profile image
David Swayze

I keep reading a lot of posts when I first get into trouble. This is how I once reached this website (kodlogs.net/1/task-delay-vs-thread...) and got the desired solution. You can read this post as well as visit here. I think it will be very useful for you

Collapse
 
nenadj profile image
Nenad

If you are worried about beginners than explain them !

Collapse
 
negue profile image
negue

If you starting a new Task.Run or having a Task in a method without something to do after it, you don't need to async/await it in the method.

Example, instead of:

public static async Task<int> MakeOneMillionDollars()
{
    return await Task.Run(() =>
    {
        Console.WriteLine("Million Dollars Made!");
                return 1000000;
    });
}

you can just write this:

public static Task<int> MakeOneMillionDollars()
{
    return Task.Run(() =>
    {
        Console.WriteLine("Million Dollars Made!");
        return 1000000;
    });
}

This removes some async/await overhead :)

Also as already mentioned Task.Delay instead of Thread.Sleep

Collapse
 
vekzdran profile image
Vedran Mandić

Good point, but also it should always be taken with great care in production scenarios that utilize try/catch in similar code as now you are risking of not having the exception unwrapped in the MakeOneMillionDollars due to not awaiting it here explicitly. But the perf tip is on point as this code will produce one compiler generated type less with its following state machine.

Collapse
 
kryptosfr profile image
Nicolas Musset • Edited

I quote "Awaiting asynchronous code, two things happen. A thread is taken from the thread pool and allocated to the asynchronous operation".

That's just wrong and a big misunderstanding of async/await. Asynchrony doesn't need to get threads from the thread pool or otherwise.

In your case it just happens because you used Task.Run but that's a edge case and actually not a good practice with async/await (Task.Run is more suited with TPL but you can use async/await when you combine with other asynchronous calls). Most asynchronous code never consumes threads.

See blog.stephencleary.com/2013/11/the...

That said I did enjoy reading the article. But that's because I'm evil!

Collapse
 
htissink profile image
Henrick Tissink

Hi, I appreciate your comment - and you are partially correct.

It's up to the scheduler to determine if a new thread is needed. It may or may not spin up a new thread. I'll clarify it when I have a chance.

Collapse
 
kryptosfr profile image
Nicolas Musset

It is a bit more than that actually. Your example demonstrates parallel computation not asynchrony. Task.Run never creates asynchronous code, whether the scheduler decides to run it immediatly on the same thread, or to schedule it on a worker.

It is understandable to be confused since most of us (me included) started to use tasks when async/await got out. But the Task type was originally created for parallel computing with the TPL (hence why it is in a System.Threading sub-namespace). Asynchronous task (also called future or promise in other languages) should have used a different type but the .Net team decided to reuse Task for that purpose. It does lead to a unified model where we can combine both, but it also adds to the confusion.

I think a better example would be to use sockets or file asynchronous reading/writing, which doesn't involve any threads.

Thread Thread
 
htissink profile image
Henrick Tissink

Thanks for the explanation (and the link) Nicolas :) you really taught me something valuable today.

Collapse
 
peledzohar profile image
Zohar Peled • Edited

Nice one, and perhaps it's just me, but I find your lack of faith distur... No, wait, I find your usage of animated gifs disturbing. IMHO, There's way too many of them in an otherwise really good and simple (and funny! that counts!) introduction to async programming.

Collapse
 
htissink profile image
Henrick Tissink

Thanks for the feedback - I always appreciate constructive criticism. I'll try and find a better balance for the gifs.

Collapse
 
mburszley profile image
Maximilian Burszley

(voice) I appreciated the gifs.

Thread Thread
 
jack profile image
Jack Williams

I read this article because of the gifs 😆

Thread Thread
 
luturol profile image
Rafael Ahrons

The gifs made it very funny to read and learn

Collapse
 
sainathsurender profile image
sainathsurender

I feel having gifs makes it interesting for beginners and makes them wanna read it till the end. Keep it up @henrick

Collapse
 
wreckitrob profile image
Rob

This is probably the first time I've fully understood async and await and the benefits it provides all while being an interesting and fun.

Thank you.

Collapse
 
htissink profile image
Henrick Tissink

Thanks for the compliment Rob :) I really appreciate it.

Collapse
 
luturol profile image
Rafael Ahrons

I loved the article!!

I've never understood async/await until now, and had to extract all those methods to another one and had it called from Main using .Wait(). I was like "Damn, that is good use and explanation" while laughing a lot

Collapse
 
vikasjk profile image
Vikas-jk

Good article, If someone stumbles upon this and want to read more about async-await in MVC, take a look
Asynchronous programming in C# and ASP.NET MVC ( With example )

Collapse
 
bellonedavide profile image
Davide Bellone

Cool!
A nice thing to know is that if the return type of a method is Task<int> you can return simply int and have .net work for you :)

code4it.dev/blog/asynchronous-prog...

Collapse
 
coderlegi0n profile image
CoderLegion

Task. Sleep is a synchronous thread that puts the thread to sleep so it can't be used for anything else. Task, on the other hand. Delay is an asynchronous thread that can be used to perform other tasks while it waits. Refer to this kodlogs.net/1/task-delay-vs-thread...

Collapse
 
lukegarrigan profile image
Luke Garrigan

Brilliant work mate, good to see some genuinely good content mixed in with humour. Thanks for this.

Collapse
 
htissink profile image
Henrick Tissink

Appreciate the positive feedback Luke! :)