DEV Community

loading...
Cover image for Asynchronous C#: Cherry on the top 🍒 (Tips and Tricks)

Asynchronous C#: Cherry on the top 🍒 (Tips and Tricks)

Paula Fahmy
I code stuff, my console's font color is not green tho 👨‍💻
Updated on ・5 min read

In Part 1 and Part 2 of the series, we took a nice dive to get us started in writing efficient async code in C#, I wanted to finalize the series with a couple of Tips and Tricks that will certainly come in handy in most development cases.

Buckle up your seatbelts 🚀..

✔ Tip #1

static async Task BoilWaterAsync()
{
    Console.WriteLine("Starting the kettle");
    await Task.Delay(3000);
    Console.WriteLine("Kettle Finished Boiling");
}

static async Task PourWaterAsync()
{
    var boilWaterTask = BoilWaterAsync();
    await boilWaterTask;
    Console.WriteLine("Pouring boiling water.");
}

static async Task Main(string[] args)
{
    // Notice we are not awaiting the task!
    PourWaterAsync();
    Console.WriteLine("Program Ended");
}

Enter fullscreen mode Exit fullscreen mode

In our first example, PourWaterAsync() represents a Task that is not awaited, although, the task consists of inner tasks that are indeed being awaited.

Running this example will result in the following output:

Starting the kettle
Program Ended
Enter fullscreen mode Exit fullscreen mode

The task Task.Delay(3000) has started, but despite awaiting it, it did not get to reach the finishing line, and the kettle did not finish boiling.
This behavior has resulted due to skipping the await keyword in the top-level Main() function.

Of course, rewriting these two lines:

var boilWaterTask = BoilWaterAsync();
await boilWaterTask;
Enter fullscreen mode Exit fullscreen mode

to be:

await BoilWaterAsync();
Enter fullscreen mode Exit fullscreen mode

.. would give the same result.
Always remember to "await" any task till reaching the very last calling point.

✔ Tip #2

public void Main(string[]args)
{
    var task = BoilWaterAsync();

    // DON'T
    var result = task.Result;

    // A NO-NO
    task.Wait();

    // PLEASE DON'T ⛔
    task.GetAwaiter().GetResult();
}


Enter fullscreen mode Exit fullscreen mode

The above three DON'Ts will cause the application's main thread to be blocked till the tasks are finished, the app might stop responding or processing new requests till the synchronous wait is completed.

✅ DO:
Embrace the asynchronous nature of the task, await it, and return the result. Tasks being propagated throughout your code is normal, just try to delay doing so as much as you can.

✔ Tip #3

Let's say we are implementing an interface, one of its methods requires a Task to be returned, BUT the execution of the method itself does not require an asynchronous setup, it would not block the main thread and can run synchronously without problems.

interface MyInterface
{
    Task<string> DoSomethingAndReturnAString();
}

class MyClass : MyInterface
{
    public Task<string> DoSomethingAndReturnAString()
    {
        // Some logic that does not need the await keyword
        return "result"; // Of course a compiler error, expecting Task<string> not a string
    }
}
Enter fullscreen mode Exit fullscreen mode

We can solve this issue with two approaches:

Solution 1 (a bad one 👎):

Convert the method to be async (even though we do not need to await anything), and now we could return a string normally, right? DON'T ever do that!

The moment you mark a method as async, the compiler transforms your code into a state machine that keeps track of things like yielding execution when an await is reached and resuming execution when a background job has finished. In fact, if you checked the IL (Intermediate Language) generated from your code after marking a method as async, you'll notice that the function has turned into an entire class for that matter.

So even on synchronous method marked with async (without await inside), a state machine will still be generated anyways and the code which could potentially be inlined and executed much faster will be generating additional complexity for state machine management.

📝 That's why it's so important to mark methods as async if and only if there is await inside.

Solution 2 (much better 👍):

Instead: return Task.FromResult("result");

Let's take another example:

We have an HTTP client retrieving some text from an external web source,

public Task<string> GetFromWebsite()
{
    var client = new HttpClient();
    var content = client.GetStringAsync("my.website.com");

    •••
}
Enter fullscreen mode Exit fullscreen mode

If the objective is to only start retrieving the string from the website, then the best way to do it is to return the task of GetStringAsync() as is, and do not await it here, it boils down to the same reason of skipping the aimless creation of a state machine.

public Task<string> GetFromWebsite()
{
    var client = new HttpClient();
    return client.GetStringAsync("my.website.com");
}
Enter fullscreen mode Exit fullscreen mode

The only case you'd want to await the call is that you want to perform some logic on the result inside the method:

// Notice that we marked the method as async
public async Task<string> GetFromWebsiteAndValidate()
{
    var client = new HttpClient();
    var result = await client.GetStringAsync("my.website.com");

    // Perform some logic on the result

    return result;
}
Enter fullscreen mode Exit fullscreen mode

Remember, if you used the word "and" while describing what a method does, something might not be right.
Example:

  • "My method is doing x and y and z" (NO-NO ⛔)
  • "I have 3 methods, one for doing x, another for y, and the last for z" (YES ✔)

This will make your life much easier when trying to apply unit tests to your code.

So a rule of thumbs 👍 to note down here:
Only await when you absolutely need the result of the task.
The async keyword does not run the method on a different thread, or do any other kind of hidden magic, hence, only mark a method as async when you need to use the keyword await in it.

A couple of notes to always remember:

  • An async function can return either on of the three types: void, Task, or Task<T>.
  • A function is "awaitable" because it returns a Task or a Task<T>, not because it is marked async, so we can await a function that is not async (a one just returning Task).
  • You cannot await a function returning void, hence, you should always return a Task unless there is an absolute reason not to do so (a caller to the function expects a void return type), or that the function itself is a top-level function and there is no way other functions will be able to call it..

✔ Tip #4

We established that long-running tasks should always execute asynchronously, these tasks can fall down into two main categories, I/O-Bound and CPU-Bound. A task is said to be I/O bound when the time taken for it to complete is determined principally by the period spent waiting for input/output operations to be completed. In contrast, a CPU-Bound task is a task that's time of completion is determined principally by the speed of the central processor, examples for these would be:

  • I/O Bound Tasks:
    • Requesting data from the network
    • Accessing the database
    • Reading/Writing to a file system
  • CPU Bound Tasks: Generally performing an expensive calculation such as:
    • Graphics Rendering
    • Video Compression
    • Heavy mathematical computations

Generally, whenever you got an I/O Bound Task or Task<T> at hands, await it in an async method. For CPU-bound code, you await an operation that is started on a background thread with the Task.Run method.

Note 📝:
Make sure you analyzed the execution of your CPU-Bound code, and be mindful of the context switching overhead when multithreading, it might be not so costly after all in comparison.

Okay, that's all I have for y'all today.
Keep Coding 😉

Discussion (8)

Collapse
drdamour profile image
chris damour • Edited

Tip #1 is inaccurate or misworded, that 3 second delay is awaited regardless if you put await on the method call or the task returned because either way you are actually putting the await on the task returned

Collapse
paulafahmy profile image
Paula Fahmy Author

Thanks for your comment, I'm not sure if I correctly understand your point, but if you try to run this code dotnetfiddle.net/WuH8FA, you'll notice that not awaiting the call on the Main method will cause the program to end before the 5 seconds period has passed, even though I awaited the delay itself: await Task.Delay(5000) and awaited
the task of boiling water
: await boilWaterTask

Collapse
drdamour profile image
chris damour

i see what you were getting at now. i find the way it's worded to be confusing, but could be a me thing. that delay is definitely awaited, but the task doesn't happen to finish before the application exits. await can be used to mean both the beginning and end state of a task, but most commonly it refers to the beginning...you await a thing and the compiler builds up the continuation for you. Here you were referring to awaiting being the continuation after the result...this isn't a great way to think about it, cause it makes people think the execution is "blocked"

Thread Thread
_siva_sankar profile image
Siva Sankaran S • Edited

I think the confusion arises because we didn't have a consensus about the very idea of "awaiting".

I will try to rephrase the word "awaiting". If there is an await operator after an async method or its returned task, the compiler will store rest of code with its context and return a task (which will contain return value of rest of code in future) immediately to caller. The rest of the code will be executed only when that async method completes .

If there is no await operator after that async method, it is like fire and forgot. Rest of the code will be executed immediately without returning of async method.

I think Paula Fahmy is saying that. Any way async method will run (some thread will work there, doing // task delay(3000);// ) but caller of non awaiting async method will forgot about it and do next thing immediately.

Correct me if I am wrong

Thread Thread
drdamour profile image
chris damour

There are awaits in the nesting, so something is being awaited. Its just the very top level that is fire and forget. Its entirely possible (near impossible of likely) the Thread scheduler schedules the awaited threads exclusively and the program DOESNT exit before all the awaited things (main would just have to be paused for 4 seconds for this to happen). So thats the correction...there is still an await happening..now if there were no awaits anywhere youd be right

Thread Thread
paulafahmy profile image
Paula Fahmy Author

Yes exactly. There IS an await, the execution IS "awaited" in the sense that a context is being captured, etc.
But since the top level caller didn't "use" the await keyword, the execution would start but there would be no guarantee that we'd capture the full expected results.
PourWaterAsync() has indeed started, the inner awaits are functioning as they should, but we did not "wait" for the results and the program continues execution regardless.
I updated this section of the article, I hope I made it clearer this time. Really appreciate the feedback! ♥

Collapse
_siva_sankar profile image
Siva Sankaran S • Edited

Apart from //"not awaiting inner methods"// What'll happen if we are not awaiting a method ?

Calling an async method without await (and without awaiting it's returned task) will not have any impact in asynchronous nature of code . Will it ?

One disadvantage is exception can't be handled if we are not awaiting. Can anyone share what else if we aren't awaiting?

Collapse
paulafahmy profile image
Paula Fahmy Author

Yes, correct, by the time the exception is thrown, the program would've passed the point of catching it.
If you absolutely need to run synchronously and for some reason don't want to use "await", you could use ".GetAwaiter().GetResult();" to be able to catch the exception gracefully, otherwise, the exception would not be thrown.
Here is a snippet if you want to try it out dotnetfiddle.net/uXBsWT