Jeremy Clark Explains Task, Await and Asynchronous Methods in C#
There's a lot of confusion about async/await, Task/TPL and asynchronous and parallel programming in general, so Jeremy Clark is on a mission to inform developers on how to use everything properly.
Microsoft documentation states: "You can avoid performance bottlenecks and enhance the overall responsiveness of your application by using asynchronous programming. However, traditional techniques for writing asynchronous applications can be complicated, making them difficult to write, debug, and maintain." That complexity is illustrated in the graphic below, depicting what happens inside of a single async method call, from said documentation (for VB).
Clark's next stop on his educational mission to help developers wade through that complexity and successfully write, debug and maintain asynchronous applications is the five-day, in-person Visual Studio Live! developer conference set to kick off at Microsoft HQ in Redmond, Wash., on July 17.
Specifically, the developer educator at JeremyBytes.com will teach the basics and look at how attendees can call asynchronous methods using Task and then see how the "await" operator can makes things easier. Along the way, he'll look at continuations, ConfigureAwait and exception handling.
Attendees at his session, I'll Get Back to You: Task, Await, and Asynchronous Methods in C#, will learn how to:
- How to get data back from an asynchronous method
- How to handle exceptions that occur in an asynchronous method
- When to use "ConfigureAwait"
We caught up with Clark to learn more about his session in a short Q&A.
VisualStudioMagazine: What's a typical pain point developers experience when getting started in asynchronous programming?
Clark: The await operator is a bit magical. This is good because it hides a lot of the details of asynchronous programming. It is also bad because it hides a lot of details of asynchronous programming. For me, once I understood more about Task (the type that we primarily "await"), I found that I could use await more confidently. It also helped me understand the limitations of await and when I need to use Task more directly.
"Another pain point is the shear vastness of the Task type. For example, the Task.ContinueWith method has over 40 overloads -- and that's just one method. This scared me away from Task for a long time."
Jeremy Clark, Developer Educator, JeremyBytes.com
Another pain point is the shear vastness of the Task type. For example, the Task.ContinueWith method has over 40 overloads -- and that's just one method. This scared me away from Task for a long time. But we don't have to know everything about Task for it to be useful. By knowing just a few properties and methods (and a few of the overloads), we can do practical things with Task. This gets us past the initial fear and into useful code. Then we can fill in the gaps as we need them.
What are the benefits of using the Task Asynchronous Programming (TAP) model?
The Task Asynchronous Programming model (TAP) gives us a good combination of power with ease-of-use. Some of the previous async models, such as the Event Asynchronous Programming model (EAP), were easy to use, but limited in their usefulness. For example, in EAP it was really difficult to deal with exceptions in the asynchronous method. At the other end, doing manual threading is very powerful but fraught with potential issues (like race conditions) that are difficult to debug. Tasks are a great middle ground. We can "await" Task easily in common scenarios, and this gives us code that looks very familiar. But we can also take advantage of the direct power of Task when we have more complex needs such as chaining tasks together, creating child tasks, or running multiple tasks in parallel.
How does the "await" operator simplify writing asynchronous methods?
When we use the "await" operator with asynchronous methods, our code looks very similar to normal, non-asynchronous code. This makes the code easier to write and also easier to understand when we come back to it later. The compiler takes care of the details, and that lets us focus on the code that is important to our application rather than the nitty-gritty of asynchronous code implementation.
What is the role of the Task and Task<TResult> types in async programming?
Task is at the core of asynchronous programming with the Task Asynchronous Programming model (TAP). A Task represents a concurrent operation, meaning a bit of code that can run at the same time as other code. Concurrent operations make up asynchronous and parallel programming. Task<TResult> is used when we are expecting a return value once a Task is complete. TResult represents the type that we are expecting to come back. So if we have "Task<string>", this means that we will have a string after the Task is complete.
How can you handle exceptions that occur in an async method?
One great thing about using the await operator is that exceptions are thrown just like in our non-async code. That means we handle exceptions the way we are used to: with a standard try/catch block. If we use Task more directly (like when we need more control over the operations), we can check properties such as Task.IsFaulted and Task.Exception to see if an exception was thrown in the async method.
What is the purpose of the ConfigureAwait method and when should you use it?
After a Task is complete, the code that follows can run on the main thread or on a different thread -- it is up to the Task Scheduler to make these decisions. ConfigureAwait lets us tell the Task Scheduler whether we care about where that code runs. Using "ConfigureAwait(false)" can give us performance improvements, particularly in library code. If we are using desktop technologies (such as .NET MAUI, WinUI, or WinForms) or older web technologies (such as WebForms), ConfigureAwait can be used to make sure we have access to the main thread when we need it. One piece of good news is that if we are using ASP.NET Core, we do not need to worry about ConfigureAwait; it actually has no effect at all.
How can you cancel an async operation or monitor its progress?
Most async methods allow us to pass a CancellationToken as an optional parameter. When we set the token to a "cancellation requested" state, we tell the asynchronous method that we would like to stop the operation without waiting for it to complete. If we await a Task that gets canceled, it will throw an OperationCanceledException that we can catch separately from other exceptions. And if we use Task directly, we can check the Task.IsCanceled or the Task.Status property to see if cancellation occurred. Either way, we can tell if the async operation was canceled and deal with it appropriately in our code.
We can use the built-in Progress<T> type to monitor progress in asynchronous methods. This has a ProgressChanged event that is fired when progress is updated inside the async method (using the Progress.Report method). Unfortunately, we do not have time to cover progress reporting in this session, but I do have sample code available 😉.
Can I use 'Task.Result' rather than 'await' so that I don't have to worry about making my current method asynchronous?
This is very tempting, but it a bad idea to check the Result property on a Task that has not completed. Checking Result will block the current thread until the Task is complete. Blocking threads is rarely a good idea (this is why we do asynchrous programming to start with). At best, we will get a performance hit. And in some situations, we will get a deadlock, meaning 2 asynchronous methods block each other and so they never complete. It is better to await the Task, or set up a Task continuation (code that runs after a Task is complete) to get that Result value.
As an additional note, some folks use "Task.GetAwaiter().GetResult()" instead. This has the same blocking behavior, and even worse, the documentation says that GetAwaiter is for internal use only. We, as application developers, should not be using this, and its behavior could change at any time.
Some people see asynchronous code as an "infection": once we start to await things, we end up with asynchronous code all the way up the stack. But I always remember a conversation I had with Kathleen Dollard when async was still fairly new. She compared async code to plumbing: You don't want your pipes to stop halfway through your house; you want them to go all the way through. Async is the same way: we want it to go all the way through our applications.
Those wanting to attend Clark's session should note that attendees who register for the VSLive! event by May 19 can save up to $400, according to the event pricing page.
About the Author
David Ramel is an editor and writer for Converge360.