.NET Core Concepts

Understanding the building blocks of .NET Core development

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?

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:

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}");
}

Further Reading