The core for asynchronous programming are the objects Task
and Task<T>
. Both of them are compatible with the keywords async
and await
.
First of all we need to identify if the code’s I/O-bound or CPU-bound.
- the code’s limited for external operations and waits for something a lot of time. Examples of this are DDBB calls, or a server’s response. In this case we have to use
async
/await
to free the thread while we wait - the code does a CPU-intensive operation. Then we move the work to another thread using
Task.Run()
so we don’t block the main thread.
async code vs parallel code
(!) Asynchronous code is not the same as parallel code (!)
- In async code you are trying to make your threads do as little work as possible. This will keep your app responsibe, capable to serve many requests at once and scale well.
- In parallel code you do the opposite. You use and keep a hold on a thread to do CPU-intensive calculations
async code
The importante of async programming is that you choose when to wait on a task. This way, you can start other tasks concurrently
In async code, one single thread can start the next task concurrently before the previous one completes.
(!) async
code doesn’t cause additional threads to be created because an async
method doesn’t run on its own thread. (!) It runs on the current synchronization context and uses time on the thread only when the method is active.
parallel code
For parallelism you need multiple threads where each thread executes a task, and all of those tasks are executed at the same time
Async / await (I/O operations)
You may avoid performance bottlenecks and enhance overall responsiveness of your app by using async
programming.
async
methods are intended to be non-blocking operations. An await
expression in an async
method doesn’t block the current thread while the awaited task is running.
Minimal example
This is for small, minimal cases where we cannot advance any other work while we wait.
public async Task NoTimeBreakfast()
{
// we start and wait for the tasks here
Bagel bagel = await GrabBagel();
Console.WriteLine("breakfast is done");
}
private async Task<Bagel> GrabBagel()
{
await Task.Delay(1000);
Console.WriteLine("grab a bagel");
}
Key example
Start task concurrently - you can split the start of the task and its await if there’s any independent work you can do meanwhiel you wait.
This delays the waiting as you start all async tasks at once, and you wait on each task only when you need the results.
public async Task CookBreakfast()
{
// this starts both tasks concurrently here ...
Task<Egg> eggsTask = FryEggsAsync();
Task<Toast> toastsTask = MakeToastWithButterAsync();
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Juice orange = PourOJ();
Console.WriteLine("orange juice is ready");
// ... we await on tasks only when they're needed
// (check below examples on Task.WhenAll and Task.WhenAny)
Toast toast = await toastsTask;
Console.WriteLine("toast is ready");
Egg eggs = await eggsTask;
Console.WriteLine("eggs are ready");
Console.WriteLine("breakfast is ready");
}
// it's possible to mix sync/async methods but you have to go async all the way up!
private Coffee PourCoffee()
{
Console.WriteLine("pouring coffee");
return new Coffee();
}
private async Task<Egg> FryEggsAsync()
{
Console.WriteLine("warming the egg pan...");
await Task.Delay(3000);
Console.WriteLine("cracking eggs");
Console.WriteLine("cooking eggs");
await Task.Delay(3000);
Console.WriteLine("eggs are done");
return new Egg();
}
private async Task<Toast> MakeToastWithButterAsync()
{
var toast = await ToastBreadAsync();
ApplyButter(toast);
return toast;
}
private async Task<Toast> ToastBreadAsync()
{
Console.WriteLine("putting slice of bread in toaster");
Console.WriteLine("start toasting...");
await Task.Delay(3000);
Console.WriteLine("remove toast from toaster");
return new Toast();
}
private Toast ApplyButter(toast)
{
Console.WriteLine("applied butter on toast");
}
Apply await expressions to tasks efficiently
We can improve the series of await
expressions at the end of previous code by using methods from Task
class.
Task.WhenAll
This returns a Task
that completes when all the tasks in a list are done
public async Task CookBreakfast()
{
// this starts both tasks concurrently here ...
Task<Egg> eggsTask = FryEggsAsync();
Task<Toast> toastsTask = MakeToastWithButterAsync();
Coffee cup = PourCofee();
Console.WriteLine("coffee is ready");
Juice orange = PourOJ();
Console.WriteLine("orange juice is ready");
// ... we await on all tasks
await Task.WhenAll(toastsTask, eggsTask);
Console.WriteLine("toast is ready");
Console.WriteLine("eggs are ready");
Console.WriteLine("breakfast is ready");
}
The following is another example for the case where we need to use the return values. In this case we use it when:
- we need to keep the same order of elements when returning
- for simple cases - it’s less complex
- but, it creates too much Tasks
public Task Main()
{
var urls = ["http://example.com", "http://example.org", "http://example.net"];
// we launch here the tasks
var readTasks = urls.ConvertAll(ReadWebpagesAsync);
// var readTasks = urls.Select(ReadWebpagesAsync).ToList();
string[] contents = await Task.WhenAll(readTasks);
// ... do something
}
private async Task<string> ReadWebpagesAsync(string url)
{
using (var client = new HttpClient())
{
return await client.GetStringAsync(url);
}
}
Task.WhenAny
Use this if you need to process or output tasks as they’re done
public async Task CookBreakfast()
{
// this starts both tasks concurrently here ...
Task<Egg> eggsTask = FryEggsAsync();
Task<Toast> toastsTask = MakeToastWithButterAsync();
Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Juice orange = PourOJ();
Console.WriteLine("orange juice is ready");
List<Task> breakfastTasks = [eggsTask, toastsTask];
while(breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if(finishedTask == eggsTask)
Console.WriteLine("eggs are ready");
if(finishedTask == toastsTask)
Console.WriteLine("toast is ready");
// this doesnt wait on the finished task, but rather waits on the Task object
// the result of this is a completed (or faulted) task
await finishedTask;
breakfastTasks.Remove(finishedTask);
}
Console.WriteLine("breakfast is ready");
}
Reference(s)
https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/
https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/task-asynchronous-programming-model
https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios
https://blog.stephencleary.com/2013/10/taskrun-etiquette-and-proper-usage.html
https://dev.to/ben-witt/task-vs-valuetask