Understanding ASP.NET Core Using Tasks and Cancellation Tokens: A Comprehensive Guide for Beginners
When developing high-performance applications in ASP.NET Core, managing asynchronous operations and resource usage efficiently is crucial. Tasks and cancellation tokens are powerful tools provided by .NET Core to handle concurrency, perform long-running operations, and provide a way to cancel these operations gracefully. In this step-by-step guide, we will dive deep into understanding how to leverage these features to build robust, scalable, and responsive applications.
What are Tasks?
In .NET, a Task represents a single operation that might run asynchronously. Tasks are part of the Task-based Asynchronous Pattern (TAP) introduced in .NET Framework 4.0 and further enhanced in subsequent versions. Tasks enable developers to perform background operations, improving application responsiveness by not blocking the main thread.
Step 1: Creating and Starting a Task
To create and start a task, you can use the Task.Run
method, which queues the task to the thread pool and returns a Task
object that represents the operation. Here's a simple example:
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Task task = Task.Run(() =>
{
// Simulate a long-running operation
Task.Delay(5000).Wait();
Console.WriteLine("Task completed.");
});
Console.WriteLine("Main thread is free to do other work.");
await task; // Await the task completion
Console.WriteLine("Task is finished and main thread can continue.");
}
}
In this example, a long-running operation is simulated using Task.Delay
. The main thread doesn't block and is free to perform other tasks while the task is running.
Step 2: Returning Values from Tasks
If you need to perform a task that returns a result, use Task<TResult>
. The result can be accessed by awaiting the task.
static async Task Main(string[] args)
{
Task<int> task = Task.Run(() =>
{
Task.Delay(5000).Wait();
return 42; // Return a result
});
Console.WriteLine("Main thread is free to do other work.");
int result = await task; // Await the task and get the result
Console.WriteLine($"Task completed with result: {result}");
}
Step 3: Handling Exceptions
When working with tasks, exceptions can occur. It's crucial to handle these exceptions properly to avoid unexpected errors.
static async Task Main(string[] args)
{
Task<int> task = Task.Run(() =>
{
Task.Delay(5000).Wait();
throw new InvalidOperationException("An error occurred in the task.");
});
Console.WriteLine("Main thread is free to do other work.");
try
{
int result = await task; // Await the task and handle exceptions
Console.WriteLine($"Task completed with result: {result}");
}
catch (AggregateException ex)
{
// Handle exception from the task
Console.WriteLine($"Exception occurred: {ex.Message}");
}
}
// Note: In modern .NET, AggregateException is flattened by 'await',
// and you can catch the inner exception directly.
catch (InvalidOperationException ex)
{
Console.WriteLine($"Exception occurred: {ex.Message}");
}
Step 4: Parallel Tasks
You may want to run multiple tasks in parallel to improve performance. Use Task.WhenAll
to wait for all tasks to complete.
static async Task Main(string[] args)
{
Task task1 = Task.Run(() =>
{
Task.Delay(3000).Wait();
Console.WriteLine("Task 1 completed.");
});
Task task2 = Task.Run(() =>
{
Task.Delay(6000).Wait();
Console.WriteLine("Task 2 completed.");
});
Console.WriteLine("Main thread is free to do other work.");
await Task.WhenAll(task1, task2); // Wait for all tasks to complete
Console.WriteLine("All tasks are finished and main thread can continue.");
}
Cancellation Tokens
A cancellation token is a mechanism to communicate a cancellation request from the main thread to long-running tasks. It helps in canceling operations gracefully without abrupt terminations.
Step 5: Setting Up a Cancellation Token
To use a cancellation token, you need to create a CancellationTokenSource
and pass its Token
to the task.
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task task = Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
// Check if cancellation has been requested
if (cts.IsCancellationRequested)
{
Console.WriteLine("Cancellation requested. Stopping the task...");
cts.Token.ThrowIfCancellationRequested();
}
// Simulate work
Task.Delay(500).Wait();
Console.WriteLine($"Working... {i + 1}");
}
}, cts.Token);
Console.WriteLine("Main thread is free to do other work. Press 'Enter' to cancel the task...");
// Wait for user input to cancel the task
Console.ReadLine();
cts.Cancel();
try
{
await task; // Await the task and handle exceptions
Console.WriteLine("Task completed successfully.");
}
catch (OperationCanceledException)
{
Console.WriteLine("Task was canceled.");
}
finally
{
// Dispose the CancellationTokenSource
cts.Dispose();
}
}
}
In this example, the CancellationTokenSource
(cts
) is used to create a cancellation token. The task checks for cancellation requests periodically using cts.IsCancellationRequested
and throws an OperationCanceledException
if cancellation is requested.
Step 6: Propagating Cancellation Tokens
Tasks can propagate cancellation tokens to other tasks, allowing you to cancel multiple tasks simultaneously. Here's an example:
static async Task Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task task1 = Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
if (cts.IsCancellationRequested)
{
Console.WriteLine("Task 1: Cancellation requested. Stopping...");
cts.Token.ThrowIfCancellationRequested();
}
Task.Delay(500).Wait();
Console.WriteLine($"Task 1 is working... {i + 1}");
}
}, cts.Token);
Task task2 = Task.Run(() =>
{
for (int i = 0; i < 5; i++)
{
if (cts.IsCancellationRequested)
{
Console.WriteLine("Task 2: Cancellation requested. Stopping...");
cts.Token.ThrowIfCancellationRequested();
}
Task.Delay(1000).Wait();
Console.WriteLine($"Task 2 is working... {i + 1}");
}
}, cts.Token);
Console.WriteLine("Main thread is free to do other work. Press 'Enter' to cancel the tasks...");
Console.ReadLine();
cts.Cancel();
try
{
await Task.WhenAll(task1, task2); // Wait for all tasks to complete
Console.WriteLine("All tasks completed successfully.");
}
catch (OperationCanceledException)
{
Console.WriteLine("Tasks were canceled.");
}
finally
{
cts.Dispose();
}
}
In this example, both task1
and task2
receive the same cancellation token, allowing them to be canceled simultaneously.
Best Practices
Use Cancellation Tokens: Always provide cancellation tokens in asynchronous methods that perform long operations, enabling users to cancel them gracefully.
Avoid Blocking Calls: Never use
.Wait()
or.Result
on tasks in ASP.NET Core, as they can lead to deadlocks. Always useawait
.Proper Exception Handling: Handle exceptions in tasks properly to avoid unhandled exceptions leading to application crashes.
Resource Management: Always dispose of
CancellationTokenSource
objects to release resources.Concurrency Control: Use
Task.WhenAll
andTask.WhenAny
to manage multiple tasks efficiently.
Real-world Example in ASP.NET Core
Let's see how to apply these concepts in a real-world ASP.NET Core application scenario.
Scenario: Downloading multiple files simultaneously with a timeout and cancellation capability.
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
public class FileDownloader
{
private readonly HttpClient _httpClient;
public FileDownloader(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task DownloadFilesAsync(List<string> urls, string downloadFolder, CancellationToken cancellationToken)
{
List<Task> tasks = new List<Task>();
foreach (var url in urls)
{
Task task = DownloadFileAsync(url, downloadFolder, cancellationToken);
tasks.Add(task);
}
try
{
// Set a timeout of 30 seconds
await Task.WhenAll(tasks).TimeoutAfter(30000, cancellationToken);
Console.WriteLine("All files downloaded successfully.");
}
catch (OperationCanceledException)
{
Console.WriteLine("File download canceled.");
}
catch (TimeoutException)
{
Console.WriteLine("File download timed out.");
}
}
private async Task DownloadFileAsync(string url, string downloadFolder, CancellationToken cancellationToken)
{
try
{
using (var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
{
response.EnsureSuccessStatusCode();
string fileName = Path.GetFileName(url);
string filePath = Path.Combine(downloadFolder, fileName);
using (var contentStream = await response.Content.ReadAsStreamAsync())
using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
{
await contentStream.CopyToAsync(fileStream, cancellationToken);
}
Console.WriteLine($"File {fileName} downloaded successfully.");
}
}
catch (OperationCanceledException)
{
Console.WriteLine($"File download for {url} canceled.");
throw;
}
catch (Exception ex)
{
Console.WriteLine($"Error downloading file {url}: {ex.Message}");
throw;
}
}
}
public static class TaskExtensions
{
public static Task TimeoutAfter(this Task task, int millisecondsTimeout, CancellationToken cancellationToken)
{
TaskDelay delay = Task.Delay(millisecondsTimeout, cancellationToken);
Task completedTask = Task.WhenAny(task, delay);
return completedTask.ContinueWith(taskResult =>
{
if (taskResult != task || task.IsFaulted || task.IsCanceled)
return;
// Throw a TimeoutException if the delay completed first
throw new TimeoutException();
});
}
}
class Program
{
static async Task Main(string[] args)
{
using (HttpClient client = new HttpClient())
{
FileDownloader downloader = new FileDownloader(client);
List<string> urls = new List<string>
{
"https://example.com/file1.pdf",
"https://example.com/file2.jpg",
"https://example.com/file3.zip"
};
CancellationTokenSource cts = new CancellationTokenSource();
Task downloadTask = downloader.DownloadFilesAsync(urls, ".", cts.Token);
Console.WriteLine("Downloading files... Press 'Enter' to cancel.");
Console.ReadLine();
cts.Cancel();
await downloadTask;
cts.Dispose();
}
}
}
Explanation:
- HttpClient: Used to download files from URLs.
- FileDownloader: A class that handles file downloads using cancellation tokens.
- DownloadFilesAsync: Downloads multiple files simultaneously and handles cancellation.
- DownloadFileAsync: Downloads a single file and handles cancellation.
- TaskExtensions.TimeoutAfter: Extension method to add a timeout to a task.
This example demonstrates the practical application of tasks and cancellation tokens in a web application, ensuring efficient and responsive operations.
Conclusion
Tasks and cancellation tokens are essential tools in ASP.NET Core for handling asynchronous operations and managing resources effectively. By understanding how to create, manage, and cancel tasks using cancellation tokens, you can build scalable and resilient applications. Remember to follow best practices to ensure your applications remain robust and efficient. Happy coding!