DEV Community

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

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

paulafahmy profile image Paula Fahmy ・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!

Enter fullscreen mode Exit fullscreen mode

Not awaiting a method call means that no inner tasks would be awaited too, so await boilWaterTask; has no effect, so does await Task.Delay(3000);.

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, the task of boiling the water would start running but would not be awaited.

✔ Tip #2

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

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

    // A NO-NO

    // PLEASE DON'T ⛔

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("");

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("");
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("");

    // 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.

  • "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 (3)

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

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, 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

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 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"

Forem Open with the Forem app