loading...

Interview question: async & await (C#)

tyrrrz profile image Alexey Golub ・4 min read

Q: What is the purpose of async/await keywords?

These keywords allow writing asynchronous non-blocking code in a synchronous fashion.

This feature is facilitated by the Task/Task<T> classes or ValueTask/ValueTask<T> structs. These types represent an abstraction around an operation that may execute asynchronously.

We use await keyword to materialize the task into resulting value. Methods that contain await must be marked with the async keyword.


Q: What's the difference between asynchronous programming and multithreaded programming?

An asynchronous task does not necessarily represent execution on a separate thread. You can think of an asynchronous operation as just an aggregation of two events -- start and finish.

A good example of an asynchronous operation is reading a file from a hard drive. To read individual bytes of a file, operating system issues requests to the driver software, which in turn tells the drive to seek to a specific position by moving its mechanical head. The process of moving the head around is asynchronous, it's not an operation that runs on CPU, it's just a physical task you have to wait for completing. This is the type of "pure" async operation that can be represented with the async/await pattern.

That said, an asynchronous task may also represent some CPU-bound calculation happening on a separate thread, but this is an implementation detail. This is useful when you want to delegate execution to a different thread in order to not block the calling thread, while disguising it as an asynchronous operation. You can do that by calling Task.Run().

Overall, it's fair to say that every multithreaded execution can be represented as an asynchronous operation, but not every asynchronous operation necessarily employs additional threads.


Q: How does it work?

Let's take the following method as an example:

public async Task DoAsync()
{
    Console.WriteLine("Before await");

    await Task.Delay(TimeSpan.FromSeconds(1));

    Console.WriteLine("Between awaits");

    await Task.Delay(TimeSpan.FromSeconds(1));

    Console.WriteLine("After await");
}

Functionally, this code works by printing "Before await" to the console, waiting 1 second, printing "Between awaits", waiting 1 second again, and then printing "After await".

Everything until the first await is executed synchronously just like it would in a normal method. That means "Before await" will be printed on the same context that called this method.

The next thing that happens is that we create and run a new task using a static helper method Task.Delay(). This is a simple task that does nothing and automatically turns into completed after a specified delay.

Upon reaching the await keyword, the runtime will return control back to the calling method, which may or may not await it. If it does await, the same happens again, returning control to the caller of that method in turn, until it reaches a method in the call stack that either does not await (usually event handlers on a message loop thread) or materializes the task synchronously (e.g. entry point in a console application).

Once the task completes (after 1 second), the execution is returned back to our method, which continues by printing "Between awaits". By default, the execution will continue on the same context as the one that started executing this method.

Then the story repeats again by running and waiting on a new task, finally printing "After await". After the last message, the execution is returned to the caller method (if it awaited on DoAsync) so that it can continue executing further.


Q: What happens if we execute an asynchronous method but don't await it?

Nothing special, the operation represented by the task will continue its lifetime normally, but the result won't be observed. The task object itself will eventually be reclaimed by garbage collector.

Note that we can also use .ContinueWith() to handle the result in a call-back fashion.


Q: What happens if an exception is thrown within an asynchronous method?

If the method is awaited, the exception will instantly propagate to the calling method, then to the caller of that method, and so on, as long as the whole chain is awaited.

Otherwise, the exception will be considered unobserved, which can lead (in some versions of the framework) to the application crashing as soon as the task is disposed by the finalizer.


Q: Is it possible to make a lambda that executes asynchronously?

Yes.

var result = await new Func<Task>(async () => await Task.Delay(100));

Q: What happens when a method returns a Task without awaiting it?

Example:

public Task WaitAsync() => SomeOtherMethodAsync();

In this case, if an exception is thrown within asynchronous part of SomeOtherMethodAsync(), the WaitAsync() method will not be listed in the stack trace.


Q: Task type implements IDisposable, when are we supposed to dispose tasks?

The Dispose() method is not supposed to be invoked manually, a task will be disposed automatically when you await it or when it's reclaimed by the GC.


Q: What is the purpose of ConfigureAwait()?

By default, after the awaited task is completed, the execution continues on the originally captured context, i.e. the same thread that invoked the method. You can override that behavior by specifying ConfigureAwait(false), indicating that the execution may continue on a different context.

It's generally recommended using ConfigureAwait(false) wherever possible as it can offer minor performance gain and help prevent deadlocks.


Q: Why is the default behavior to continue on the captured context?

For compatibility reasons, due to constraints when working with Windows UI and classic ASP.NET applications. In one you can only interact with controls from the main thread, in the other you can only serve the response on the same thread that handled the request, so it was important that the execution continued on the captured context.


Q: What is the difference between Task and ValueTask?

The main difference is that Task is a class while ValueTask is a struct. Latter was added to the BCL to alleviate unnecessary pressure on the garbage collector caused by asynchronous methods that often return synchronously (e.g. cached result). You can think of ValueTask as a discriminated union of Task or a synchronous result. Since recently, ValueTask can also represent a result signaled by ValueTaskSource.


Q: What are the main downsides of using asynchronous methods compared to synchronous methods?

Asynchronous methods can be harder to debug, especially because exceptions thrown from asynchronous methods have difficult to read stack traces. Also, running many asynchronous tasks in a tight loop can put pressure on the garbage collector.

Posted on by:

tyrrrz profile

Alexey Golub

@tyrrrz

Filtering data with a cheesegrater

Discussion

pic
Editor guide
 

"..The Dispose() method is not supposed to be invoked manually, a task will be disposed automatically when you await it or when it's reclaimed by the GC..."

This is valid for Finalize() not for Dispose()

 

"It's generally recommended using ConfigureAwait(false) wherever possible..."

I don't think it is. When you are writing application you generally want the default behavior. E.g. in WPF you want to execute the continuation/callback/code-after-await on the UI thread.

It is generally recommended to use ConfigureAwait(false) when writing general-purpose library code.

Source: devblogs.microsoft.com/dotnet/conf...

 

How about Task.Yield ?

 

Quick question, how do you create that group of links at the top of your post that links to other parts of the series?