Task-based Asynchronous Programming (TAP)
The modern programming model in C# streamlines the process of developing asynchronous code.
C# introduced an asynchronous programming model (APM) that facilitates the execution of I/O-bound operations in an asynchronous manner.
The Asynchronous Programming Model (APM) is founded on the concept of callbacks:
A method representing an asynchronous operation that accepts a callback function.
Upon completion of the asynchronous operation, the method invokes the callback to inform the calling code.
Due to the complexity of APM, C# introduced the Event-based Asynchronous Pattern (EAP) to handle asynchronous operations using events.
Unlike APM, EAP raises an event upon the completion of an asynchronous operation instead of invoking a callback.
EAP is easier to use and offers improved error handling compared to APM. Nevertheless, EAP itself remains quite complex to implement.
To address this issue, C# introduced Task-based Asynchronous Programming (TAP), which significantly simplifies asynchronous programming and facilitates the writing of asynchronous code.
TAP comprises the following key components:
The Task class – Represents an asynchronous operation.
The async/await keywords – Define asynchronous methods and await the completion of asynchronous operations.
Task-based API – A collection of classes that integrate smoothly with the Task class and the async/await keywords.
TAP has the following advantages:
Improved performance – TAP can enhance an application's performance by enabling it to execute I/O-bound operations asynchronously, thereby freeing up the CPU for other tasks.
Simplified code – TAP allows you to write asynchronous code in a manner similar to synchronous code, making it easier to understand.
Better resource management – TAP optimizes system resources by allowing applications to perform asynchronous operations without blocking threads.
Let's explore how to utilize it for executing asynchronous operations.
The Task class serves as a fundamental concept within TAP. It represents an asynchronous operation that can be executed through various methods.
Let's consider a method named DoSomethingAsync()
that conducts an asynchronous operation, simulates an asynchronous operation that takes 2 seconds, like this:
static async Task DoSomethingAsync()
{
Console.WriteLine("Performing some asynchronous operation...");
await Task.Delay(2000);
Console.WriteLine("Asynchronous operation completed!");
}
In contrast to a typical function, DoSomethingAsync()
incorporates Task.Delay()
to introduce a two-second delay before executing the entire operation. The intent behind Task.Delay()
is to emulate an asynchronous operation requiring approximately two seconds to finalize.
Executing a task
To execute the DoSomethingAsync()
method asynchronously, you instantiate a new Task
object and invoke the DoSomethingAsync()
method within a lambda expression provided to the Task
constructor:
Task.Run(() => DoSomethingAsync());
Put it all together:
static async Task DoSomethingAsync()
{
Console.WriteLine("Performing some asynchronous operation...");
await Task.Delay(2000);
Console.WriteLine("Asynchronous operation completed!");
}
Task.Run(() => DoSomethingAsync());
Console.WriteLine("Start the program...");
Console.ReadLine();
Output:
Start the program...
Performing some asynchronous operation...
Asynchronous operation completed!
Please note that the task.Start()
method doesn't block the main thread. As a result, you may observe the following message first:
Start the program...
….before the asynchronous operation:
Performing some asynchronous operation...
Asynchronous operation completed!
The Console.ReadLine()
method blocks the main thread until a key is pressed. It's employed to wait for the child thread, scheduled by the Task
object, to complete.
Failing to block the main thread will lead to its termination after displaying the message "Start the program...".
It's worth noting that the Task
constructor accepts various options that are unnecessary to consider at this stage.
Behind the scenes, the program leverages a thread pool to execute the asynchronous operation. The Start()
method schedules the operation for execution.
To demonstrate this, we can display the thread ID and indicate whether the thread is part of the managed thread pool:
static async Task DoSomethingAsync()
{
var threadId = Thread.CurrentThread.ManagedThreadId;
var threadPool = Thread.CurrentThread.IsThreadPoolThread;
Console.WriteLine($"The thread #{threadId}, use a thread pool {threadPool}");
Console.WriteLine("Performing some asynchronous operation...");
await Task.Delay(2000);
Console.WriteLine("Asynchronous operation completed!");
}
Task.Run(() => DoSomethingAsync());
Console.WriteLine("Start the program...");
Console.ReadLine();
Output:
Start the program...
The thread #11, use a thread pool True
Performing some asynchronous operation...
Asynchronous operation completed!
The output indicates that the thread ID is 11 and confirms that the thread belongs to a thread pool. Note that you may see a different number.
Since the process of creating and starting a Task object is rather verbose, you can simplify it by using the Run()
static method of the Task
class:
Task.Run(() => DoSomethingAsync());
The Run()
method queues the operation (DoSomethingAsync
) to the thread pool for execution.
Similarly, you can use the StartNew()
method of the Task
class's Factory object to create a new task and schedule its execution:
Task.Factory.StartNew(() => DoSomethingAsync());
Summary
Use task-based asynchronous programming (TAP) for developing asynchronous programs.
Use the
Task
class to execute asynchronous operations.