In 2012, C#5 was released. This version introduced two new keywords async
and await
. At that time CPU clock speed reached an upper limit imposed by physical laws. But chip makers started to deliver CPU with several cores that can run tasks in parallel. Thus C# needed a away to ease asynchronous programming.
The async
and await
keywords make asynchronous programming almost too easy. Many programmers use them often without really understanding the runtime workflow. This is a great thing, they can focus more on the business of their applications and less on asynchronous details. But some disconcerting behaviors might (and will) happen. Thus it is preferable that one understands the logic behind async
and await
and what can influence it. This is the goal of the present article.
Here is a small C# program that illustrates the async
and await
keywords. Two tasks A and B runs simultaneously. Task A runs within a method marked as async
, while B is executed after calling the async
method.
Before explaining in details how the two occurrences of the keyword async
modify the workflow, let’s make some remarks:
First let’s explain the easy role of the async
keyword. Then we’ll have a closer look at the influence of the await
keyword.
C# Async / Await – Make your app more responsive and faster with asynchronous programming
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");
}
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));
Task
without awaiting it?Example:
public Task WaitAsync() => SomeOtherMethodAsync();
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.
1. Can you explain what asynchronous programming is?
Asynchronous programming is a form of programming that allows for tasks to be completed out of order. This means that a program can start a task and then move on to other tasks before the first one is completed. This can be helpful in situations where a task might take a long time to complete, but the program doesn’t need to wait for it to finish before moving on.
2. How does async/await help with performance and scalability?
Async/await can help improve performance and scalability by allowing your application to do other work while it is waiting for a task to complete. This can help avoid bottlenecks and keep your application responsive. Additionally, using async/await can help reduce the overall amount of code needed to be written, making your application easier to maintain.
3. Can you explain the difference between an async function and a regular function in JavaScript?
Async functions are functions that allow you to use the await keyword to wait for a promise to resolve before continuing execution of the function. Regular functions do not have this ability, and will instead execute the code inside of them immediately.
4. What are some of the advantages of using async functions in JavaScript?
Async functions help to make code simpler and easier to read. They also can make code execution more efficient by allowing tasks to be run in parallel.
5. What do you understand about await in JavaScript?
The await keyword in JavaScript is used to pause the execution of a function until a Promise is resolved. This allows you to write asynchronous code that looks and feels like synchronous code.
Overview of the asynchronous model
The core of async programming is the Task
and Task
objects, which model asynchronous operations. They are supported by the async
and await
keywords. The model is fairly simple in most cases:
- For I/O-bound code, you await an operation that returns a
Task
orTask
inside of anasync
method. - For CPU-bound code, you await an operation that is started on a background thread with the Task.Run method.
The await
keyword is where the magic happens. It yields control to the caller of the method that performed await
, and it ultimately allows a UI to be responsive or a service to be elastic. While there are ways to approach async code other than async
and await
, this article focuses on the language-level constructs.
History[edit]
F# added asynchronous workflows with await points in version 2.0 in 2007.[6] This influenced the async/await mechanism added to C#.[7]
Microsoft released a version of C# with async/await for the first time in the Async CTP (2011). And were later officially released in C# 5 (2012).[8]
Haskell lead developer Simon Marlow created the async package in 2012.[9]
Python added support for async/await with version 3.5 in 2015[10] adding 2 new keywords, async
and await
.
TypeScript added support for async/await with version 1.7 in 2015.[11]
Javascript added support for async/await in 2017 as part of ECMAScript 2017 JavaScript edition.
Rust added support for async/await with version 1.39.0 in 2019 [12] with 1 new keyword async
and a lazy eval await pattern.[13]
C++ added support for async/await with version 20 in 2020 with 3 new keywords co_return
, co_await
, co_yield
.
Swift added support for async/await with version 5.5 in 2021, adding 2 new keywords async
and await
. This was released alongside a concrete implementation of the Actor model with the actor
keyword[14] which uses async/await to mediate access to each actor from outside.
The magic behind the C# await keyword
Now that we detailed the await
keyword workflow we can measure how powerful it is. Some magic does occur under the hood to resume the execution once the task finishes. Let’s have a look at the thread stack trace after await taskA;
in the main method.
123456789 | ...ConsoleWriteLine(“Wait for taskA termination”);awaittaskA; Console.WriteLine(newSystem.Diagnostics.StackTrace()); ConsoleWriteLine($“The result of taskA is {taskA.Result}”);Console.ReadKey();} |
Here it is:
The simple line await taskA;
leads the C# compiler to generate a lot of code to pilot the runtime. Identifiers like AsyncState...
and MoveNext()
shows that a state machine is created for us to let the magic of code continuation happens seamlessly. Here is the assembly decompiled with ILSpy. We can see that a class is generated by the compiler for each usage of the await
keyword:
Here is a call graph generated by NDepend of the methods of the Task Parallel Library (TPL) called by the generated code. To obtain such graph with methods and fields generated by the compiler, the following setting must be disabled first: NDepend > Project Properties > Analysis > Merge Code Generated by Compiler into Application Code
The details of what the C# compiler generates when it meets the keyword await
is outside the scope of this article but you can deep dive in it in this Microsoft article. Just keep in mind that the code executed after an await
keyword can eventually be executed by a random thread and that a lot of code that calls the TPL is generated to make this happen. Let’s explain how the random thread is chosen by he runtime.
So far we only demonstrated code executed in the context of a console application. The context in which some asynchronous code runs actually influences its workflow a lot. For example let’s run the same code in the context of a WPF application. Since it is convenient to keep the console output to show results of our experiments, let’s set the output type of our WPF assembly to Console Application, so a console is shown when the WPF app starts.
Now let’s execute the exact same code from within a WPF button click event handler:
12345678 | publicpartial classMainWindow:Window{publicMainWindow(){InitializeComponent();}privateasyncvoidButton_Click(objectsender,RoutedEventArgse){ConsoleWriteLine($“Start Program”);Task<int>taskA =MethodAAsync();... |
Here is the surprising result: the main thread is used to run everything! And task A loops are postponed after task B loops (except the first one).
This is totally different than what we had with our console application. The key is that in a WPF context (and also in a Winforms context) there is a synchronization context object, that can be obtained through SynchronizationContext.Current
.
There is no synchronization context in a console application.
FAQ
Does C have async await?
How does async work in C?
What is async await?
What is C# await?