Tasks in .NET Core
In .NET Core, a Task
represents an asynchronous operation. It's a fundamental construct for writing responsive and scalable applications, especially in scenarios involving I/O-bound operations (like network requests or file access) or CPU-bound operations that can be offloaded to background threads.
The System.Threading.Tasks
namespace provides the core types for task-based asynchronous programming (TAP). The most central type is System.Threading.Tasks.Task
itself, and its generic counterpart, System.Threading.Tasks.Task<TResult>
, which represents an asynchronous operation that returns a value.
Why Use Tasks?
- Responsiveness: Prevents UI freezes by offloading work from the main thread.
- Scalability: Efficiently manages threads, allowing applications to handle more concurrent operations without consuming excessive resources.
- Simplicity: Provides a more streamlined and composable way to manage asynchronous operations compared to older models like callbacks or events.
- Composability: Tasks can be easily chained, combined, and waited upon, making complex asynchronous workflows manageable.
Creating and Running Tasks
Tasks can be created using the Task.Run()
method, which is a convenient way to execute a delegate (like a lambda expression) on a thread pool thread.
// Non-generic Task
Task longRunningTask = Task.Run(() => {
// Simulate a long-running operation
System.Threading.Thread.Sleep(3000);
Console.WriteLine("Operation completed on thread: " + Thread.CurrentThread.ManagedThreadId);
});
// Generic Task (returns a value)
Task<int> taskWithResult = Task.Run(() => {
// Simulate work and return a result
System.Threading.Thread.Sleep(2000);
return 42;
});
Console.WriteLine("Tasks created.");
// Optional: Wait for tasks to complete
// longRunningTask.Wait();
// int result = taskWithResult.Result;
// Console.WriteLine("Result: " + result);
Waiting for Tasks
You can wait for a task to complete using methods like Wait()
, WaitAll()
, or WaitAny()
. However, using await
is generally preferred for better composition and avoiding deadlocks in asynchronous contexts.
// Using await (preferred)
await longRunningTask;
int result = await taskWithResult;
Console.WriteLine($"Result obtained via await: {result}");
// Using Wait() - blocks the current thread
// longRunningTask.Wait();
// int resultBlocking = taskWithResult.Result; // .Result also blocks if not completed
// Console.WriteLine($"Result obtained via Wait(): {resultBlocking}");
Note: .Result
and .Wait()
will block the calling thread until the task completes. This can be detrimental to application responsiveness, especially in UI applications. Prefer await
for non-blocking waits.
Task Continuation
You can schedule code to run after a task completes using continuations. This is often done with ContinueWith()
or by using await
.
Task processDataTask = Task.Run(() => {
// Process some data
Console.WriteLine("Data processing started...");
System.Threading.Thread.Sleep(1500);
Console.WriteLine("Data processing finished.");
return "Processed Data";
});
// Using ContinueWith
processDataTask.ContinueWith(antecedent => {
// This continuation runs after processDataTask completes
Console.WriteLine($"Continuation executed. Task Status: {antecedent.Status}");
if (antecedent.Status == TaskStatus.RanToCompletion) {
Console.WriteLine($"Data: {antecedent.Result}");
}
}, TaskContinuationOptions.OnlyOnRanToCompletion); // Options to control when it runs
// Using await within another async method
async Task ProcessAndLogAsync() {
string data = await processDataTask;
Console.WriteLine($"Logging: {data}");
}
// Call the async method
// await ProcessAndLogAsync();
Task Status
Tasks have a lifecycle represented by their Status
property. Common statuses include:
Created
: The task was instantiated but not scheduled.WaitingForActivation
: The task is ready to run but not yet started.Running
: The task is executing.WaitingToRun
: The task has been scheduled but is waiting for a thread.WaitingForChildrenToComplete
: The task has completed, but is waiting for child tasks it spawned to finish.RanToCompletion
: The task completed successfully.Canceled
: The task was canceled.Faulted
: The task completed with an exception.
Cancellation
Tasks can be canceled using the CancellationTokenSource
and CancellationToken
types.
async Task DoWorkWithCancellationAsync(CancellationToken cancellationToken) {
Console.WriteLine("Starting work...");
for (int i = 0; i < 10; i++) {
// Check for cancellation regularly
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"Working... step {i + 1}");
await Task.Delay(500, cancellationToken); // Task.Delay respects cancellation
}
Console.WriteLine("Work finished successfully.");
}
// Example of using cancellation
var cts = new CancellationTokenSource();
var workTask = DoWorkWithCancellationAsync(cts.Token);
// Simulate canceling the task after a delay
Task.Delay(2500).ContinueWith(_ => cts.Cancel());
try {
await workTask;
} catch (OperationCanceledException) {
Console.WriteLine("Operation was cancelled.");
} catch (Exception ex) {
Console.WriteLine($"An error occurred: {ex.Message}");
}