Asynchronous Programming

async/await, tasks vs threads, cancellation, parallelism, and the patterns and pitfalls of asynchronous .NET.

async is a modifier that marks a method as asynchronous, indicating it can contain asynchronous operations.

await is an operator that suspends the execution of an async method until the awaited task completes, without blocking the thread.

How it works:

  • When await is encountered, the method returns control to its caller
  • The thread is freed to do other work
  • When the awaited task completes, execution resumes from where it left off

Example:

// Basic async/await example
public class DataService
{
    // Async method must return Task, Task, or void (avoid void except for event handlers)
    public async Task GetDataAsync()
    {
        // Simulate a network call or database operation
        await Task.Delay(2000); // Non-blocking delay
        return "Data retrieved successfully";
    }
    
    public async Task CalculateAsync(int x, int y)
    {
        // Simulate CPU-intensive work
        await Task.Run(() =>
        {
            Thread.Sleep(1000);
        });
        
        return x + y;
    }
}

// Calling async methods
public class Program
{
    public static async Task Main(string[] args)
    {
        var service = new DataService();
        
        Console.WriteLine("Starting async operation...");
        
        // await suspends execution until GetDataAsync completes
        string result = await service.GetDataAsync();
        Console.WriteLine(result);
        
        // Multiple async operations
        int sum = await service.CalculateAsync(5, 10);
        Console.WriteLine($"Sum: {sum}");
    }
}

// Real-world example: Fetching data from an API
public class WeatherService
{
    private readonly HttpClient httpClient = new HttpClient();
    
    public async Task GetWeatherAsync(string city)
    {
        try
        {
            Console.WriteLine($"Fetching weather for {city}...");
            
            // await makes the HTTP call non-blocking
            string response = await httpClient.GetStringAsync(
                $"https://api.weather.com/forecast?city={city}"
            );
            
            Console.WriteLine("Weather data received");
            return response;
        }
        catch (HttpRequestException ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
            return null;
        }
    }
    
    // Processing multiple cities concurrently
    public async Task<List> GetWeatherForMultipleCitiesAsync(List cities)
    {
        var tasks = cities.Select(city => GetWeatherAsync(city));
        
        // Wait for all tasks to complete
        string[] results = await Task.WhenAll(tasks);
        
        return results.ToList();
    }
}

// Usage
var weatherService = new WeatherService();
var cities = new List { "New York", "London", "Tokyo" };
var weatherData = await weatherService.GetWeatherForMultipleCitiesAsync(cities);

Key Points:

  • async methods should be named with the Async suffix by convention
  • await can only be used inside async methods
  • async void should be avoided (except for event handlers) - use async Task instead
  • Exception handling works naturally with try/catch blocks

Thread is a lower-level construct that represents an actual OS thread. It's part of the threading infrastructure.

Task is a higher-level abstraction that represents an asynchronous operation. It doesn't necessarily map to a single thread.

Example:

// Using Thread (lower-level, more control, more overhead)
public class ThreadExample
{
    public void RunWithThread()
    {
        Thread thread = new Thread(() =>
        {
            Console.WriteLine($"Thread ID: {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(2000);
            Console.WriteLine("Thread work completed");
        });
        
        thread.Start();
        thread.Join(); // Wait for thread to complete
    }
    
    // Creating multiple threads
    public void RunMultipleThreads()
    {
        for (int i = 0; i < 5; i++)
        {
            int taskNumber = i;
            Thread thread = new Thread(() =>
            {
                Console.WriteLine($"Thread {taskNumber} executing");
                Thread.Sleep(1000);
            });
            thread.Start();
        }
    }
}

// Using Task (higher-level, better performance, easier to use)
public class TaskExample
{
    public async Task RunWithTaskAsync()
    {
        await Task.Run(() =>
        {
            Console.WriteLine($"Task on Thread ID: {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(2000);
            Console.WriteLine("Task work completed");
        });
    }
    
    // Creating multiple tasks
    public async Task RunMultipleTasksAsync()
    {
        var tasks = new List();
        
        for (int i = 0; i < 5; i++)
        {
            int taskNumber = i;
            tasks.Add(Task.Run(() =>
            {
                Console.WriteLine($"Task {taskNumber} executing on Thread {Thread.CurrentThread.ManagedThreadId}");
                Thread.Sleep(1000);
            }));
        }
        
        await Task.WhenAll(tasks); // Wait for all tasks to complete
    }
    
    // Task with return value
    public async Task CalculateAsync()
    {
        return await Task.Run(() =>
        {
            Thread.Sleep(1000);
            return 42;
        });
    }
}

// Comparison example
public class ComparisonDemo
{
    public void CompareThreadAndTask()
    {
        // Thread approach - manual management
        var threads = new List();
        for (int i = 0; i < 10; i++)
        {
            var thread = new Thread(() => DoWork());
            threads.Add(thread);
            thread.Start();
        }
        
        foreach (var thread in threads)
        {
            thread.Join(); // Wait for completion
        }
        
        // Task approach - automatic management
        var tasks = new List();
        for (int i = 0; i < 10; i++)
        {
            tasks.Add(Task.Run(() => DoWork()));
        }
        
        Task.WaitAll(tasks.ToArray()); // Wait for completion
    }
    
    private void DoWork()
    {
        Thread.Sleep(500);
    }
}

// Task with proper async/await pattern
public class AsyncPatternExample
{
    public async Task FetchDataAsync()
    {
        // This doesn't block the calling thread
        await Task.Delay(1000);
        return "Data fetched";
    }
    
    public async Task ProcessDataAsync()
    {
        Console.WriteLine("Start processing");
        
        // Multiple async operations
        var task1 = FetchDataAsync();
        var task2 = FetchDataAsync();
        var task3 = FetchDataAsync();
        
        // Wait for all to complete
        string[] results = await Task.WhenAll(task1, task2, task3);
        
        Console.WriteLine($"Processed {results.Length} items");
    }
}

Key Differences:

AspectThreadTask
LevelLow-level OS constructHigh-level abstraction
Resource UsageHeavy (1 MB stack per thread)Lightweight
PoolingNo built-in poolingUses ThreadPool
Return ValueCannot return values easilyCan return values via Task<T>
Exception HandlingComplexIntegrated with async/await
CancellationManual implementationBuilt-in via CancellationToken
ComposabilityDifficultEasy with Task.WhenAll, WhenAny
Use CaseLow-level threading controlMost async operations

Task.Run() is the simpler, modern method for starting a task. It's the recommended approach for most scenarios.

Task.Factory.StartNew() provides more control and configuration options but is more complex and has some gotchas.

Example:

public class TaskCreationComparison
{
    // Task.Run() - Simple and recommended
    public async Task RunWithTaskRunAsync()
    {
        // Task.Run() always uses TaskScheduler.Default (ThreadPool)
        var result = await Task.Run(() =>
        {
            Console.WriteLine($"Task.Run on thread: {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
            return 42;
        });
        
        Console.WriteLine($"Result: {result}");
    }
    
    // Task.Run() with async lambda
    public async Task RunWithAsyncLambdaAsync()
    {
        // Task.Run properly unwraps async lambdas
        var result = await Task.Run(async () =>
        {
            await Task.Delay(1000);
            return "Completed";
        });
        
        Console.WriteLine(result);
    }
    
    // Task.Factory.StartNew() - More control but complex
    public async Task RunWithFactoryStartNewAsync()
    {
        // Requires explicit unwrapping for async lambdas
        var task = Task.Factory.StartNew(() =>
        {
            Console.WriteLine($"Factory.StartNew on thread: {Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(1000);
            return 42;
        });
        
        var result = await task;
        Console.WriteLine($"Result: {result}");
    }
    
    // Task.Factory.StartNew() with options
    public async Task RunWithOptionsAsync()
    {
        var task = Task.Factory.StartNew(
            () =>
            {
                Console.WriteLine("Long-running task started");
                Thread.Sleep(5000);
                return "Done";
            },
            CancellationToken.None,
            TaskCreationOptions.LongRunning, // Creates dedicated thread
            TaskScheduler.Default
        );
        
        var result = await task;
        Console.WriteLine(result);
    }
    
    // Demonstrating the async lambda gotcha with Factory.StartNew
    public async Task DemonstrateGotchaAsync()
    {
        // WRONG: This returns Task<Task>, not Task
        Task<Task> outerTask = Task.Factory.StartNew(async () =>
        {
            await Task.Delay(1000);
            return "Result";
        });
        
        // Need to unwrap manually
        Task innerTask = await outerTask;
        string result = await innerTask;
        
        // OR use Unwrap()
        var unwrappedTask = Task.Factory.StartNew(async () =>
        {
            await Task.Delay(1000);
            return "Result";
        }).Unwrap();
        
        result = await unwrappedTask;
        
        // Task.Run handles this automatically - CORRECT WAY
        result = await Task.Run(async () =>
        {
            await Task.Delay(1000);
            return "Result";
        });
    }
}

// Practical examples showing when to use each
public class PracticalExamples
{
    // Use Task.Run for most scenarios
    public async Task<List> ProcessDataAsync(List data)
    {
        // Offload CPU-intensive work to thread pool
        return await Task.Run(() =>
        {
            return data.Select(x => x * x).ToList();
        });
    }
    
    // Use Task.Factory.StartNew for long-running tasks
    public Task StartBackgroundServiceAsync()
    {
        return Task.Factory.StartNew(
            () =>
            {
                while (true)
                {
                    // Long-running background work
                    Console.WriteLine("Service running...");
                    Thread.Sleep(5000);
                }
            },
            TaskCreationOptions.LongRunning // Gets dedicated thread
        );
    }
    
    // Use Task.Factory.StartNew with custom scheduler
    public async Task RunWithCustomSchedulerAsync()
    {
        var scheduler = new CustomTaskScheduler();
        
        var task = Task.Factory.StartNew(
            () =>
            {
                Console.WriteLine("Running with custom scheduler");
                return 100;
            },
            CancellationToken.None,
            TaskCreationOptions.None,
            scheduler
        );
        
        var result = await task;
        Console.WriteLine($"Result: {result}");
    }
}

// Custom task scheduler example
public class CustomTaskScheduler : TaskScheduler
{
    protected override IEnumerable GetScheduledTasks()
    {
        return Enumerable.Empty();
    }
    
    protected override void QueueTask(Task task)
    {
        ThreadPool.QueueUserWorkItem(_ => TryExecuteTask(task));
    }
    
    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        return TryExecuteTask(task);
    }
}

Key Differences:

AspectTask.Run()Task.Factory.StartNew()
SimplicitySimple, recommendedComplex, more options
Default SchedulerThreadPool (TaskScheduler.Default)Can specify custom scheduler
Async LambdaAutomatically unwrapsReturns Task<Task<T>> - needs Unwrap()
Task OptionsLimited optionsFull TaskCreationOptions
Long-RunningNot idealSupports LongRunning option
When to Use99% of scenariosCustom schedulers, long-running tasks

Recommendation: Use Task.Run() unless you specifically need the advanced features of Task.Factory.StartNew().

ConfigureAwait(false) tells the awaited task not to capture and resume on the original synchronization context. This improves performance and avoids potential deadlocks in library code.

When to use:

  • In library code (not UI code)
  • When you don't need to return to the original context
  • To improve performance
  • To avoid deadlocks

Example:

// Understanding SynchronizationContext
public class SynchronizationContextExample
{
    // WITHOUT ConfigureAwait(false) - captures context
    public async Task GetDataWithContextAsync()
    {
        Console.WriteLine($"Before await - Thread: {Thread.CurrentThread.ManagedThreadId}");
        
        // By default, await captures the current SynchronizationContext
        await Task.Delay(1000);
        
        // Resumes on the same context (same thread in UI apps)
        Console.WriteLine($"After await - Thread: {Thread.CurrentThread.ManagedThreadId}");
        
        return "Data with context";
    }
    
    // WITH ConfigureAwait(false) - doesn't capture context
    public async Task GetDataWithoutContextAsync()
    {
        Console.WriteLine($"Before await - Thread: {Thread.CurrentThread.ManagedThreadId}");
        
        // ConfigureAwait(false) tells it not to capture context
        await Task.Delay(1000).ConfigureAwait(false);
        
        // Can resume on any thread pool thread
        Console.WriteLine($"After await - Thread: {Thread.CurrentThread.ManagedThreadId}");
        
        return "Data without context";
    }
}

// Library code example - SHOULD use ConfigureAwait(false)
public class DataLibrary
{
    private readonly HttpClient httpClient = new HttpClient();
    
    // Good: Library code using ConfigureAwait(false)
    public async Task FetchDataAsync(string url)
    {
        // Library code doesn't need UI context
        var response = await httpClient.GetAsync(url).ConfigureAwait(false);
        
        // Still on thread pool thread (not UI thread)
        var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
        
        // Process data without needing UI thread
        var processedData = ProcessData(content);
        
        return processedData;
    }
    
    public async Task<List> FetchMultipleAsync(List urls)
    {
        var results = new List();
        
        foreach (var url in urls)
        {
            // Each await uses ConfigureAwait(false)
            var data = await FetchDataAsync(url).ConfigureAwait(false);
            results.Add(data);
        }
        
        return results;
    }
    
    private string ProcessData(string data)
    {
        // CPU-bound processing
        return data.ToUpper();
    }
}

// UI/Application code example - DON'T use ConfigureAwait(false)
public class UserInterfaceCode
{
    // Bad: Don't use ConfigureAwait(false) in UI code
    public async Task UpdateUIAsync()
    {
        var library = new DataLibrary();
        
        // Get data (library uses ConfigureAwait(false) internally)
        var data = await library.FetchDataAsync("https://api.example.com/data");
        
        // We're back on UI thread here, can update UI safely
        // Don't use ConfigureAwait(false) here!
        UpdateTextBox(data);
    }
    
    private void UpdateTextBox(string text)
    {
        // This needs to run on UI thread
        Console.WriteLine($"Updating UI with: {text}");
    }
}

// Deadlock prevention example
public class DeadlockExample
{
    // This can cause a deadlock in UI apps
    public string GetDataSync()
    {
        // .Result blocks and waits for task
        // But task tries to resume on UI thread which is blocked
        // DEADLOCK!
        return GetDataAsync().Result;
    }
    
    private async Task GetDataAsync()
    {
        await Task.Delay(1000); // Tries to resume on UI thread
        return "Data";
    }
    
    // Fix with ConfigureAwait(false)
    public string GetDataSyncFixed()
    {
        return GetDataAsyncFixed().Result; // Still not ideal, but won't deadlock
    }
    
    private async Task GetDataAsyncFixed()
    {
        await Task.Delay(1000).ConfigureAwait(false); // Won't try to resume on UI thread
        return "Data";
    }
}

// Complete example showing best practices
public class BestPracticesExample
{
    // Library/Service layer - use ConfigureAwait(false)
    public class OrderService
    {
        public async Task GetOrderAsync(int orderId)
        {
            await Task.Delay(100).ConfigureAwait(false);
            
            var order = await FetchFromDatabaseAsync(orderId).ConfigureAwait(false);
            var details = await FetchOrderDetailsAsync(orderId).ConfigureAwait(false);
            
            order.Details = details;
            return order;
        }
        
        private async Task FetchFromDatabaseAsync(int id)
        {
            await Task.Delay(50).ConfigureAwait(false);
            return new Order { Id = id, CustomerName = "John Doe" };
        }
        
        private async Task<List> FetchOrderDetailsAsync(int orderId)
        {
            await Task.Delay(50).ConfigureAwait(false);
            return new List
            {
                new OrderDetail { ProductName = "Widget", Quantity = 2 }
            };
        }
    }
    
    // UI/Controller layer - DON'T use ConfigureAwait(false)
    public class OrderController
    {
        private readonly OrderService orderService = new OrderService();
        
        public async Task DisplayOrderAsync(int orderId)
        {
            // No ConfigureAwait(false) here - we need UI context
            var order = await orderService.GetOrderAsync(orderId);
            
            // Can safely update UI because we're on UI thread
            Console.WriteLine($"Order for: {order.CustomerName}");
            Console.WriteLine($"Items: {order.Details.Count}");
        }
    }
    
    public class Order
    {
        public int Id { get; set; }
        public string CustomerName { get; set; }
        public List Details { get; set; }
    }
    
    public class OrderDetail
    {
        public string ProductName { get; set; }
        public int Quantity { get; set; }
    }
}

Guidelines:

  • Use ConfigureAwait(false) in:

    • Library/framework code
    • Service layer methods
    • Any code that doesn't need to return to the original context
  • DON'T use ConfigureAwait(false) in:

    • UI event handlers
    • ASP.NET Core controllers (post .NET Core 2.0+ - no sync context anyway)
    • Code that needs to update UI elements
    • Top-level application code

A deadlock occurs when two or more operations are waiting for each other to complete, causing the application to freeze indefinitely.

Common async/await deadlock scenario: Blocking on async code (using .Result or .Wait()) in a context with a synchronization context (like UI applications).

Example:

// DEADLOCK EXAMPLES

// Example 1: Classic UI Deadlock
public class UIDeadlockExample
{
    // THIS CAUSES A DEADLOCK IN UI APPLICATIONS
    public void ButtonClick()
    {
        // UI thread blocks here waiting for result
        var result = GetDataAsync().Result;
        
        // Never reaches here - DEADLOCK!
        Console.WriteLine(result);
    }
    
    private async Task GetDataAsync()
    {
        // Simulates async operation (HTTP call, database query, etc.)
        await Task.Delay(1000);
        
        // Tries to resume on UI thread, but UI thread is blocked waiting!
        // DEADLOCK!
        return "Data";
    }
    
    // Explanation:
    // 1. UI thread calls GetDataAsync().Result and blocks
    // 2. GetDataAsync starts and awaits Task.Delay
    // 3. When Task.Delay completes, it tries to resume on UI thread
    // 4. But UI thread is blocked waiting for the result
    // 5. DEADLOCK - each is waiting for the other
}

// Example 2: ASP.NET Deadlock (pre-.NET Core)
public class AspNetDeadlockExample
{
    // THIS CAUSES A DEADLOCK IN ASP.NET (not ASP.NET Core)
    public ActionResult Index()
    {
        // ASP.NET request thread blocks here
        var data = GetDataAsync().Result;
        
        return View(data);
    }
    
    private async Task GetDataAsync()
    {
        await Task.Delay(1000);
        // Tries to resume on ASP.NET synchronization context
        return "Data";
    }
}

// SOLUTIONS TO DEADLOCKS

// Solution 1: Use async all the way (BEST)
public class AsyncAllTheWayExample
{
    // Proper async implementation
    public async Task ButtonClickAsync()
    {
        // Don't block - await instead
        var result = await GetDataAsync();
        Console.WriteLine(result);
    }
    
    private async Task GetDataAsync()
    {
        await Task.Delay(1000);
        return "Data";
    }
}

// Solution 2: Use ConfigureAwait(false) in library code
public class ConfigureAwaitSolution
{
    public void ButtonClick()
    {
        // Still not ideal, but won't deadlock
        var result = GetDataAsync().Result;
        Console.WriteLine(result);
    }
    
    private async Task GetDataAsync()
    {
        // ConfigureAwait(false) prevents capturing sync context
        await Task.Delay(1000).ConfigureAwait(false);
        
        // Won't try to resume on UI thread
        return "Data";
    }
}

// Solution 3: Run on thread pool
public class ThreadPoolSolution
{
    public void ButtonClick()
    {
        // Move work to thread pool
        Task.Run(async () =>
        {
            var result = await GetDataAsync();
            
            // Need to marshal back to UI thread for UI updates
            Application.Current.Dispatcher.Invoke(() =>
            {
                Console.WriteLine(result);
            });
        });
    }
    
    private async Task GetDataAsync()
    {
        await Task.Delay(1000);
        return "Data";
    }
}

// COMPLEX DEADLOCK SCENARIOS

// Scenario 1: Nested async calls
public class NestedDeadlockExample
{
    public void ProcessData()
    {
        // Blocking on first async method
        var result = FirstMethodAsync().Result;
    }
    
    private async Task FirstMethodAsync()
    {
        // This method awaits another async method
        var data = await SecondMethodAsync();
        return data.ToUpper();
    }
    
    private async Task SecondMethodAsync()
    {
        await Task.Delay(1000);
        // Deadlock occurs here trying to resume
        return "data";
    }
}

// Scenario 2: Multiple blocking calls
public class MultipleBlockingExample
{
    public void ProcessMultiple()
    {
        // Each of these can cause a deadlock
        var result1 = GetData1Async().Result;
        var result2 = GetData2Async().Result;
        var result3 = GetData3Async().Result;
    }
    
    private async Task GetData1Async()
    {
        await Task.Delay(100);
        return "Data1";
    }
    
    private async Task GetData2Async()
    {
        await Task.Delay(100);
        return "Data2";
    }
    
    private async Task GetData3Async()
    {
        await Task.Delay(100);
        return "Data3";
    }
}

// CORRECT PATTERNS

// Pattern 1: Async all the way up
public class CorrectAsyncPattern
{
    // Controller/UI method is async
    public async Task ProcessDataAsync()
    {
        var result1 = await GetData1Async();
        var result2 = await GetData2Async();
        var result3 = await GetData3Async();
        
        Console.WriteLine($"{result1}, {result2}, {result3}");
    }
    
    private async Task GetData1Async()
    {
        await Task.Delay(100);
        return "Data1";
    }
    
    private async Task GetData2Async()
    {
        await Task.Delay(100);
        return "Data2";
    }
    
    private async Task GetData3Async()
    {
        await Task.Delay(100);
        return "Data3";
    }
}

// Pattern 2: Library code with ConfigureAwait
public class LibraryCodePattern
{
    public async Task GetUserDataAsync(int userId)
    {
        // All awaits use ConfigureAwait(false)
        var user = await FetchUserAsync(userId).ConfigureAwait(false);
        var orders = await FetchOrdersAsync(userId).ConfigureAwait(false);
        var preferences = await FetchPreferencesAsync(userId).ConfigureAwait(false);
        
        return new UserData
        {
            User = user,
            Orders = orders,
            Preferences = preferences
        };
    }
    
    private async Task FetchUserAsync(int id)
    {
        await Task.Delay(100).ConfigureAwait(false);
        return new User { Id = id, Name = "John" };
    }
    
    private async Task<List> FetchOrdersAsync(int userId)
    {
        await Task.Delay(100).ConfigureAwait(false);
        return new List();
    }
    
    private async Task FetchPreferencesAsync(int userId)
    {
        await Task.Delay(100).ConfigureAwait(false);
        return new Preferences();
    }
    
    public class User { public int Id { get; set; } public string Name { get; set; } }
    public class Order { }
    public class Preferences { }
    public class UserData
    {
        public User User { get; set; }
        public List Orders { get; set; }
        public Preferences Preferences { get; set; }
    }
}

// Detecting deadlocks
public class DeadlockDetection
{
    public void DetectDeadlock()
    {
        try
        {
            // Set a timeout to detect potential deadlock
            var task = GetDataAsync();
            
            if (!task.Wait(TimeSpan.FromSeconds(5)))
            {
                Console.WriteLine("Potential deadlock detected!");
            }
        }
        catch (AggregateException ex)
        {
            Console.WriteLine($"Error: {ex.InnerException?.Message}");
        }
    }
    
    private async Task GetDataAsync()
    {
        await Task.Delay(1000);
        return "Data";
    }
}

Key Takeaways:

  1. Never block on async code - Don't use .Result or .Wait() in UI or ASP.NET contexts
  2. Async all the way - Make your methods async from top to bottom
  3. Use ConfigureAwait(false) in library code
  4. ASP.NET Core is safer - It doesn't have a synchronization context, reducing deadlock risk
  5. Be careful with Task.WaitAll() - Same issues as .Result