This file is part of a multi-series workshop on learning C# on Linux available here.
Authors: Phillip Carter, Ken Chen, Martin Woodward
Async programming is a first-class concept within .NET, with async support in the runtime,
the framework libraries, and .NET language constructs including in C#. Internally, they are based off of objects
(such as Task
) which take advantage of the underlying operating system to perform I/O-bound jobs
as efficiently as possible. In client applications this is useful to ensure the UI thread remains responsive
and for server applications having first class native support for asynchronous programming is a key factor in
helping developers build scalable web applications.
The C# language-level asynchronous programming model follows what is known as the
Task-based Asynchronous Pattern (TAP).
The core of TAP are the Task
and Task<T>
objects, which model asynchronous operations, supported
by the async
and await
keywords (Async
and Await
in VB), which provide a natural developer experience
for interacting with Tasks. The result is the ability to write expressive asynchronous code, as opposed to
callback methods which often do not express the intent as cleanly. There are other ways to approach
async code than async
and await
outlined in the TAP article linked above, but this tutorial will focus
on the language-level constructs in C#.
For example, you may need to download some data from a remote web service when a button is pressed, but don’t want to block the UI thread. It can be accomplished simply like this:
private readonly HttpClient _httpClient = new HttpClient();
...
button.Clicked += async (o, e) =>
{
var stringData = await _httpClient.DownloadStringAsync(URL);
DoStuff(stringData);
};
And that’s it! The code expresses the intent (downloading some data asynchronously) without getting bogged down in interacting with Task objects.
For the theoretically-inclined, this is an implementation of the Future/Promise concurrency model.
A few important things to know before continuing:
- Async code uses the
Task<T>
andTask
types, which are constructs used to model the work being done in an asynchronous context. - When the
await
keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete. This is what allows a UI to be responsive and a service to be elastic. await
can only be used inside an async method.- Unless an async method has an
await
inside its body, it will never yield! async void
should only be used on Event Handlers (where it is required).
The following example shows how to write basic async code for both a client app and a web service. The code, in both cases, will count the number of times ”.NET” appears in the HTML of “dotnetfoundation.org”.
private readonly HttpClient _httpClient = new HttpClient();
private async void SeeTheDotNets_Click(object sender, RoutedEventArgs e)
{
// Capture the task handle here so we can await the background task later.
var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("http://www.dotnetfoundation.org");
// Any other work on the UI thread can be done here, such as enabling a Progress Bar.
// This is important to do here, before the "await" call, so that the user
// sees the progress bar before execution of this method is yielded.
NetworkProgressBar.IsEnabled = true;
NetworkProgressBar.Visibility = Visibility.Visible;
// The await operator suspends SeeTheDotNets_Click, returning control to its caller.
// This is what allows the app to be responsive and not hang on the UI thread.
var html = await getDotNetFoundationHtmlTask;
int count = Regex.Matches(html, ".NET").Count;
DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";
NetworkProgressBar.IsEnabled = false;
NetworkProgressBar.Visbility = Visibility.Collapsed;
}
private readonly HttpClient _httpClient = new HttpClient();
[HttpGet]
[Route("DotNetCount")]
public async Task<int> GetDotNetCountAsync()
{
// Suspends GetDotNetCountAsync() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await _httpClient.GetStringAsync("http://dotnetfoundation.org");
return Regex.Matches(html, ".NET").Count;
}
using System;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace ConsoleApplication
{
public class Program
{
public static readonly string ADDRESS = "http://dotnetfoundation.org";
public static int Main(string[] args)
{
HttpClient httpClient = new HttpClient();
Task<string> getStringTask = Task.Run(async() =>
{
return await httpClient.GetStringAsync(ADDRESS);
});
Console.WriteLine("Connecting to {0}...", ADDRESS);
Console.WriteLine("This may take a moment");
Console.WriteLine("Number of times \".NET\" used: {0}",
Regex.Matches(getStringTask.Result, ".NET").Count);
return 1;
}
}
}
As mentioned before, Tasks are constructs used to represent operations working in the background.
Task
represents a single operation which does not return a value.Task<T>
represents a single operation which returns a value of typeT
.
Tasks are awaitable, meaning that at the await
will allow your application or service to perform useful
work while the task is running by yielding control to its caller until the task is done.
If you’re using Task<T>
, the await
keyword will additionally “unwrap” the value returned when the Task is
complete.
It’s important to reason about Tasks as abstractions of work happening in the background, and not an
abstraction over multithreading. In fact, unless explicitly started on a new thread via Task.Run
, a Task will
start on the current thread and delegate work to the Operating System.
To understand the detail of what happens during an async call let's examine the example previously.
The call (such as GetStringAsync
from HttpClient
) makes its way through the .NET libraries and runtime
until it reaches a system interop call (such as P/Invoke
to call out to the underling OS).
This eventually makes the proper System API call (such as write()
to a socket file descriptor on Linux).
That System API call is then dealt with in the kernel, where the I/O request is sent to the proper subsystem.
Although details about scheduling the work on the appropriate device driver are different for each OS,
eventually an “incomplete task” signal will be sent from the device driver, bubbling its way back up to the .NET
runtime. This will be converted into a Task
or Task<T>
by the runtime and returned to the calling method.
When await
is encountered, execuction is yielded and the system will continue in the calling method
while the Task is running.
When the device driver has the data, it sends an interrupt which eventually allows the OS to bubble the
result back up to the runtime, which will the queue up the result of the Task.
Eventually execution will return to the method which called GetStringAsync
at the await
,
and will “unwrap” the return value from the Task<string>
which was being awaited.
The method now has the result!
Although many details were glossed over here (such as how “borrowing” compute time on a thread pool is coordinated), the important thing to recognize here is that this is an interrupt based system and no thread is 100% dedicated to running the initiated task. This allows threads in the thread pool of a system to handle a larger volume of work rather than having to wait for I/O to finish.
Although the above may seem like a lot of work to be done, when measured in terms of wall clock time, it’s miniscule compared to the time it takes to do the actual typical I/O operation.
Tasks are also used outside of the async programming model. They are the foundation of the Task Parallel Library, which supports the parallelization of CPU-bound work via Data Parallelism and Task Parallelism.
Although async programming is relatively straightforward in C#, there are some rules to keep in mind which can prevent unexpected behavior.
This is the naming convention used in .NET to more-easily differentiate synchronous and asynchronous methods. Note that certain methods which aren’t explicitly called by your code (such as event handlers or web controller methods) don’t necessarily apply. Because these are not explicitly called by your code, being explicit about their naming isn’t as important.
async void
is the only way to allow asynchronous event handlers to work because events do not have
return types (thus cannot make use of Task
and Task<T>
). Any other use of async void
does not follow
the TAP model and can be challenging to use, such as:
- Exceptions thrown in an
async void
method can’t be caught outside of that method. async void
methods are very difficult to test.async void
methods can cause bad side effects if the caller isn’t expecting them to be async.
I know we've only just learned them and the temptation will be to used all these new tricks at ones but Lambda expressions in LINQ use deferred execution, meaning code could end up executing at a time when you’re not expecting it to. The introduction of blocking tasks into this can easily result in a deadlock if not written correctly. Additionally, the nesting of asynchronous code like this can also make it more difficult to reason about the execution of the code. Async and LINQ are powerful, but should be used together as carefully and clearly as possible.
Blocking the current thread as a means to wait for a Task to complete can result in deadlocks and blocked context threads, and can require significantly more complex error-handling. The following table provides guidance on how to deal with waiting for Tasks in a non-blocking way:
Use this... | Instead of this... | When wishing to do this |
---|---|---|
await |
Task.Wait or Task.Result |
Retrieving the result of a background task |
await Task.WhenAny |
Task.WaitAny |
Waiting for any task to complete |
await Task.WhenAll |
Task.WaitAll |
Waiting for all tasks to complete |
await Task.Delay |
Thread.Sleep |
Waiting for a period of time |
Don’t depend on the state of global objects or the execution of certain methods. Instead, depend only on the return values of methods. Why?
- Code will be easier to reason about.
- Code will be easier to test.
- Mixing async and synchronous code is far simpler.
- Race conditions can typically be avoided altogether.
- Depending on return values makes coordinating async code simple.
- (Bonus) it works really well with dependency injection.
A recommended goal is to achieve complete or near-complete Referential Transparency in your code. Doing so will result in an extremely predictable, testable, and maintainable codebase.
-
Write a console application that initializes a new
DoSomething
class below, executes theDoSomething.Slow()
method then theDoSomething.Fast()
method and then displays the resultSlow()
on one line followed by the result ofFast()
on the next. Run the application and observe the results. -
Create a new
DoSomething.SlowAsync()
method that does the same as theSlow()
method below but instead ofThread.Sleep(5000);
waits for 5 seconds using the following code insteadawait Task.Delay(5000);
. Modify your application to call the new Async version instead, execute and observe the results. -
Adjust the amount of delay in the
Slow()
method and increase the work done in theFast()
method until you observe the fast method thread finishing after the slow method thread and observe the behaviour.
Sample Code
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication
{
public class DoSomething
{
public int Slow()
{
Console.WriteLine("Start: Slow operation on thread {0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(5000);
Console.WriteLine("End: Slow operation on thread id {0}", Thread.CurrentThread.ManagedThreadId);
return 42;
}
public int Fast()
{
Console.WriteLine("Start: Fast operation on thread {0}", Thread.CurrentThread.ManagedThreadId);
int[] fib = new int[] { 0, 1, 1, 2, 3, 5, 8, 13 };
foreach (int element in fib)
{
System.Console.WriteLine(element);
}
Console.WriteLine("End: Fast operation on thread id {0}", Thread.CurrentThread.ManagedThreadId);
return 1;
}
}
}
Dependencies For the exercises, you will also need the following dependencies in your project.json
"dependencies": {
"System.Threading.Tasks": "4.3.0",
"System.Threading.Thread": "4.3.0"
},
The use of async
and await
in C# helps .NET optimize the usage of processing threads and allows your application
to feel more responsive to users and to have better scalability characteristics without introducing significant
additional complexity to your code. The .NET framework takes care of thread pooling and thread synchronization for the developer
but you still have to be explicit with your code when you want to make use of asynchronous programming. Consider
using Async when calling any potentially long running I/O operations or other long, non-CPU bound activities.
- Async/Await Reference Docs
- Tasks and the Task Parallel Library
- Async Programming : Introduction to Async/Await on ASP.NET
- Next: Tutorial 8 - Web Programming with ASP.NET Core
- Previous: Tutorial 6 - Linq
- Back to Table of Contents