My perspective on the async/await feature introduced in C# 5.0 has been somewhat conflicted. While I’ve always been impressed by the compiler magic that makes it work, its uniqueness to C# initially made me prefer using ContinueWith. Using Tasks in this way felt similar to Promises in JavaScript. However, with ES7 incorporating async/await and Python already having it, I’ve realized it’s time to embrace async/await fully. Although numerous excellent articles and tutorials on the topic exist, I’m documenting my basic understanding here as a reference for myself.
Central to async/await is the concept of Task (and Task
Tasks, while similar in promising future completion, offer explicit control over thread creation via Task.Run. This method leverages the TPL (Task Parallel Library) to create a new thread (typically from the ThreadPool) for the provided code to execute. Directly using “new Thread()” is discouraged in favor of this approach. The article at This reading provides further insights.
In essence, a Task embodies the promise of work to be completed at some point. Tasks can be created in several ways:
- Using Task.Run or Task.Factory.StartNew (without flags): Queues the work on the .NET ThreadPool.
- Using Task.Factory.StartNew with TaskCreationOptions.LongRunning: Signals the TaskScheduler to spawn a dedicated thread instead of using the ThreadPool.
- Leveraging the async-await feature (Promise Tasks) in C# 5: When awaiting a method returning a Task or Task
, these methods might not utilize threading mechanisms like the ThreadPool or new Thread if they rely on asynchronous APIs. Instead, they execute on the calling thread and yield control back upon encountering the first “await” keyword. - (Contributed by Servy): Creating a custom TaskScheduler and providing it to Task.Factory.StartNew. This offers granular control over Task execution.
It’s crucial to remember that “await” doesn’t independently create threads or magically make a method asynchronous. The awaited method must return a Task, with “async/await” merely simplifying the handling of these Tasks. As aptly stated by someone at here, “The new Async and await keywords allow you to orchestrate concurrency in your applications. They don’t actually introduce any concurrency into your application.”
Fundamentals
A method containing the “await” keyword must be marked as “async”; otherwise, a compilation error (CS4032) occurs: “Error CS4032 The ‘await’ operator can only be used within an async method. Consider marking this method with the ‘async’ modifier and changing its return type to ‘Task>’.”
While calling an “async” method without “await” is possible (using Task.Wait/Task.Result instead), it requires caution. Deadlocks can occur unless ConfigureAwait(false) is used, or a ContinueWith is employed in the caller. Further details on this can be found here.
To grasp how “await” works, envision it as implicitly calling ContinueWith on the Task. This ContinueWith is passed a lambda containing the code following the “await.” Multiple “await” calls within a method can be seen as chaining continuations. Refer to this question for a practical example. Based on my understanding from this chapter, the actual mechanism is more complex, involving pushing the lambda and having the Awaiter invoke ContinueWith. Nonetheless, the core concept remains similar.
An analogy can be drawn between “yield” and “await,” both involving significant compiler transformations to create state machines. Both keywords suspend the execution of the current method (without suspending the thread). The crucial difference lies in how the suspended method resumes. Methods suspended by “yield” are restarted explicitly (using “Next” on the generated enumerator), while methods suspended by “await” magically resume upon completion of the awaited Task.
The parallels between “yield” and “await” extend further. Similar to the IEnumerable and IEnumerator pair, there’s an Awaitable and Awaiter pair. While the Awaiter is usually hidden, it manages the Awaitable (checking completion status and continuations). This resembles using a “foreach” loop to iterate an IEnumerable, where the IEnumerator handles the actual iteration transparently. here provides an insightful perspective on the “duck typing” nature of these patterns.
Avoid using:
| |
Instead, prefer:
| |
The latter is more efficient, utilizing a Timer internally instead of needlessly consuming a ThreadPool thread for sleeping.
I plan to delve into Synchronization Contexts in a future post.