Latest update Android YouTube

Chapter 10: Common Async Pitfalls & Anti-Patterns (Deadlocks, Thread Starvation, Misuse of Tasks)

Introduction

In this chapter, we'll cover:

  • Sync-over-async problems
  • Deadlocks
  • Async void disasters
  • Task.Run misuse
  • Blocking shared thread pool
  • Misconfigured ValueTask
  • Forgetting ConfigureAwait
  • Hidden backpressure & queuing issues
  • Async in constructors
  • Overawaiting & underawaiting
  • I/O vs CPU confusion
  • Returning cold tasks incorrectly

All of these cause real production outages if ignored.

10.1 The #1 Async Killer: Sync-over-Async (Blocking .Result / .Wait())

❌ Dangerous Code

var user = _service.GetUserAsync(id).Result;

or:

_service.SaveAsync().Wait();

Why This Breaks:

  • It blocks the calling thread
  • Can cause deadlocks
  • Causes ThreadPool starvation
  • Cancels scalability benefits of async
  • Makes async code behave like slow synchronous code

Always Use:

var user = await _service.GetUserAsync(id);

10.2 The Classic Deadlock Scenario (Very Common in .NET)

❌ This code DEADLOCKS:

public User GetUser() 
{ 
    return _repo.GetUserAsync().Result; // Deadlock 
}

Why?

  • The .Result blocks the thread
  • await inside GetUserAsync() tries to resume on the same context
  • That context is blocked → DEADLOCK

Fix:

public async Task<User> GetUserAsync() 
{ 
    return await _repo.GetUserAsync(); 
}

10.3 Using async void: The Worst Anti-Pattern

❌ Never do this:

public async void Process() 
{ 
    await Task.Delay(1000); 
}

Reasons:

  • Exceptions cannot be caught
  • Crash the process silently
  • Impossible to test
  • Breaks reliability
  • Not awaitable

Only acceptable place for async void:

✔ Event handlers (UI events in desktop apps—NOT in APIs)

10.4 Misusing Task.Run in ASP.NET Core

❌ Never offload I/O work to Task.Run

await Task.Run(() => _db.SaveChanges()); // WRONG

Why?

  • Wastes a pool thread
  • Adds expensive thread switch
  • Breaks scalability
  • Causes thread starvation under load

Only use Task.Run for:

  • ✔ CPU-bound parallel workloads in non-ASP.NET apps
  • ✔ Background worker apps (not API request pipeline)

APIs must rely on async I/O, not Task.Run.

10.5 Blocking the ThreadPool (Huge Production Issue)

❌ Dangerous patterns:

Thread.Sleep(1000); // Blocks thread
Task.Delay(1000).Wait(); // Blocks thread

Effects:

  • Ties up server threads
  • Incoming requests queue
  • High latency
  • Server timeouts

Always use:

await Task.Delay(1000);

10.6 Forgetting to Propagate CancellationToken

❌ Wrong

public Task<List<Order>> GetOrdersAsync() 
{ 
    return _db.Orders.ToListAsync(); // No token 
}

Why it's critical:

  • Long-running DB queries continue after client disconnect
  • Resources wasted
  • Orphaned background work
  • May overload DB under high traffic

Correct:

public Task<List<Order>> GetOrdersAsync(CancellationToken token) 
{ 
    return _db.Orders.ToListAsync(token); 
}

10.7 Not Using ConfigureAwait in Library Code

What ConfigureAwait(false) Does:

  • Avoids capturing synchronization context
  • Reduces deadlock risk
  • Improves performance
  • Recommended for libraries (not web apps)

Example:

await SomeInternalAsyncMethod().ConfigureAwait(false);

⚠️ In ASP.NET Core, synchronization context does not exist

→ So ConfigureAwait doesn't affect correctness, only micro-performance.

10.8 Misunderstanding ValueTask (Can Break Your App)

ValueTask is tricky.

❌ Mistake 1: Returning ValueTask without need

public ValueTask<int> GetAsync() 
{ 
    return new ValueTask<int>(_repo.GetCountAsync()); // WRONG 
}

This is actually worse than using Task.

❌ Mistake 2: Consuming ValueTask multiple times

var x = await valueTask; 
var y = await valueTask; // FAIL – ValueTask may not be reusable

When ValueTask should be used:

  • ✔ High-performance hot paths
  • ✔ Frequently synchronous completion
  • ✔ Avoiding allocations

❌ NOT for general async code

10.9 Fire-and-Forget Inside Controllers (Silent Failures)

❌ Wrong:

Task.Run(() => _service.SendEmailAsync()); // No await, no logging, no retry
return Ok();

Problems:

  • Task may crash
  • No exception handling
  • Lost logs
  • Worker may die during app shutdown
  • Data inconsistency

Correct:

Use a background queue or IHostedService (covered in Chapter 9).

10.10 Writing Async Code Inside Constructors (Not Allowed)

Constructors cannot be async.

❌ Wrong:

public class Foo 
{ 
    public Foo() 
    { 
        await InitAsync(); // Invalid 
    } 
}

Correct patterns:

  • ✔ Use async initialization method
  • ✔ Use factory method
  • ✔ Use Lazy<Task<T>>

10.11 Serial Await When Parallelism Is Needed

❌ Slow code:

var a = await GetAAsync(); 
var b = await GetBAsync(); 
var c = await GetCAsync();

Runs sequentially → slow.

✔ Correct:

var aTask = GetAAsync(); 
var bTask = GetBAsync(); 
var cTask = GetCAsync(); 

await Task.WhenAll(aTask, bTask, cTask); 

var a = aTask.Result; 
var b = bTask.Result; 
var c = cTask.Result;

Huge performance win.

10.12 Parallel ForEach DOES NOT support async by default

❌ Wrong:

Parallel.ForEach(users, async user => 
{ 
    await ProcessUserAsync(user); 
});

This creates:

  • Unobserved tasks
  • Lost exceptions
  • Messy behavior

✔ Correct pattern:

await Task.WhenAll(users.Select(u => ProcessUserAsync(u)));

10.13 Over-Awaiting (await inside tight loops)

❌ Slow:

foreach (var item in items) 
{ 
    await Process(item); 
}

✔ Better:

await Task.WhenAll(items.Select(item => Process(item)));

10.14 Not Handling Exceptions inside WhenAll

❌ Wrong:

await Task.WhenAll(tasks); // exceptions aggregated silently

Correct:

try 
{ 
    await Task.WhenAll(tasks); 
} 
catch (Exception ex) 
{ 
    // ex.InnerExceptions contains all failures 
}

10.15 Async Without Async (Fake Async / Fake I/O)

❌ Wrong:

public async Task<int> FakeAsync() 
{ 
    Thread.Sleep(1000); // blocking 
    return 1; 
}

This is worse than synchronous.

✔ Correct:

public async Task<int> RealAsync() 
{ 
    await Task.Delay(1000); 
    return 1; 
}

10.16 Returning Cold Tasks Incorrectly

❌ Wrong:

public Task<int> GetValue() => new Task<int>(() => 42);

This returns a non-started task. Consumers get stuck.

✔ Correct:

public Task<int> GetValue() => Task.FromResult(42);

10.17 Forgetting async all the way down

❌ Wrong pattern:

Controller: async
Service: sync
Repository: sync
DB: sync

Async only at the top does nothing.

✔ Correct:

Controller → Service → Repository → DB → Network

Every layer must be async.

10.18 Large Object Allocations in Async Loops

Allocating large lists inside async loops causes memory pressure.

❌ Wrong:

foreach (...) 
    var buffer = new byte[1024 * 1024];

Use pooling:

ArrayPool<byte>.Shared.Rent(...)

10.19 Common Performance Mistakes

❌ Using async when not needed (CPU-bound work)

Async is useless for CPU-only logic.

Use:

  • Parallel.ForEach
  • ValueTask
  • Task.Run in background workers (NOT in APIs)

10.20 The Ultimate Anti-Pattern: Mixing async + blocking + sync

This is how outages happen:

await _repo.GetAsync(); 
var data = _http.GetAsync().Result; // mixed await + sync → DEADLOCK
_SlowFileWrite().Wait(); // blocked thread

Never mix sync and async.

10.21 Async Best Practices Checklist

  • ✔ Always use async all the way down
  • ✔ Never use .Result or .Wait()
  • ✔ Avoid async void
  • ✔ Don't use Task.Run in controllers
  • ✔ Use CancellationToken everywhere
  • ✔ Use Task.WhenAll for concurrency
  • ✔ Use ValueTask only in optimized paths
  • ✔ Avoid blocking calls inside async loops
  • ✔ Always await async tasks
  • ✔ Stream large data instead of loading fully
  • ✔ Handle aggregated exceptions properly
  • ✔ Never hide exceptions
  • ✔ Never fire-and-forget in API pipeline
  • ✔ Never block ThreadPool threads

10.22 Thread Pool Starvation: Diagnosis and Prevention

How to Detect Thread Pool Starvation

// Diagnostic method to check thread pool health
public static void LogThreadPoolStatus(ILogger logger)
{
    ThreadPool.GetAvailableThreads(out int workerThreads, out int completionPortThreads);
    ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxCompletionPortThreads);
    
    var workerUsage = ((double)(maxWorkerThreads - workerThreads) / maxWorkerThreads) * 100;
    var ioUsage = ((double)(maxCompletionPortThreads - completionPortThreads) / maxCompletionPortThreads) * 100;
    
    logger.LogWarning(
        "ThreadPool status: Worker threads {UsedWorker}/{MaxWorker} ({WorkerPct}%), " +
        "IOCP threads {UsedIO}/{MaxIO} ({IOPct}%)",
        maxWorkerThreads - workerThreads, maxWorkerThreads, workerUsage,
        maxCompletionPortThreads - completionPortThreads, maxCompletionPortThreads, ioUsage);
}

Warning signs:

  • Worker threads usage > 80%
  • IOCP threads usage > 80%
  • Queue length growing continuously
  • Increasing response times under load

Thread Pool Injection Anti-Pattern

// ❌ WRONG: Causes thread pool injection (slow and dangerous)
public async Task ProcessManyItemsAsync(List<Item> items)
{
    var tasks = items.Select(item => Task.Run(async () =>
    {
        // Each Task.Run creates new thread pool work item
        await ProcessItemAsync(item);
    }));
    
    await Task.WhenAll(tasks); // Thread pool injection!
}

// ✅ CORRECT: Use natural async parallelism
public async Task ProcessManyItemsCorrectAsync(List<Item> items)
{
    var tasks = items.Select(item => ProcessItemAsync(item));
    await Task.WhenAll(tasks); // No thread pool injection
}

Thread Pool Configuration Tuning

// Programmatic thread pool configuration (use cautiously!)
public static void ConfigureThreadPool()
{
    // Only adjust if you have concrete evidence of starvation
    ThreadPool.SetMinThreads(
        workerThreads: 100, 
        completionPortThreads: 100);
    
    // Monitor for 1-2 minutes before deciding to adjust
    // Defaults are usually fine for most applications
}

10.23 Async Lock Patterns and Deadlocks

❌ The Async Lock Deadlock Pattern

private readonly object _syncLock = new object();

public async Task<int> GetValueAsync()
{
    lock (_syncLock) // ❌ Blocking lock in async method
    {
        var value = await GetValueFromDbAsync(); // Deadlock risk
        return Process(value);
    }
}

✅ Correct: Async-compatible locks

// Option 1: SemaphoreSlim (preferred for async)
private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);

public async Task<int> GetValueAsync()
{
    await _asyncLock.WaitAsync();
    try
    {
        var value = await GetValueFromDbAsync();
        return Process(value);
    }
    finally
    {
        _asyncLock.Release();
    }
}

// Option 2: AsyncLock from Nito.AsyncEx
private readonly AsyncLock _asyncLock = new AsyncLock();

public async Task<int> GetValueAsync()
{
    using (await _asyncLock.LockAsync())
    {
        var value = await GetValueFromDbAsync();
        return Process(value);
    }
}

Deadlock Detection with Timeout

public async Task<T> ExecuteWithTimeoutAsync<T>(
    Func<Task<T>> operation, 
    TimeSpan timeout)
{
    using var cts = new CancellationTokenSource(timeout);
    
    try
    {
        return await operation().WaitAsync(cts.Token);
    }
    catch (OperationCanceledException) when (cts.IsCancellationRequested)
    {
        throw new TimeoutException($"Operation timed out after {timeout}");
    }
}

// Usage
await ExecuteWithTimeoutAsync(
    () => _repository.GetDataAsync(),
    TimeSpan.FromSeconds(5));

10.24 Task Continuation Pitfalls

❌ Wrong: Continuation that blocks

// This continuation runs on thread pool but blocks
var task = SomeAsyncOperation()
    .ContinueWith(previousTask =>
    {
        // ❌ Blocking call in continuation
        var result = previousTask.Result;
        Thread.Sleep(1000); // Blocks thread pool thread
        return Process(result);
    });

✅ Correct: Async continuation

// Use ContinueWith with TaskContinuationOptions
var task = SomeAsyncOperation()
    .ContinueWith(async previousTask =>
    {
        // ✅ Proper async continuation
        var result = await previousTask;
        await Task.Delay(1000); // Non-blocking
        return await ProcessAsync(result);
    }, TaskContinuationOptions.OnlyOnRanToCompletion)
    .Unwrap(); // Important: Unwrap nested task

Continuation Context Capture

// ❌ Captures synchronization context (deadlock risk in WinForms/WPF)
var task = SomeAsyncOperation()
    .ContinueWith(t => 
    {
        UpdateUI(); // Runs on UI thread
    }, TaskScheduler.FromCurrentSynchronizationContext());

// ✅ Use ConfigureAwait in async methods instead
public async Task ProcessAsync()
{
    var result = await SomeAsyncOperation().ConfigureAwait(false);
    // Run continuation on thread pool
    await ProcessResultAsync(result);
}

10.25 Async State Machine Performance Overhead

The Hidden Cost of Async State Machines

// Each async method creates a state machine allocation
public async Task<int> ExpensiveAsync()
{
    // State machine allocation: ~100 bytes
    var a = await GetAAsync(); // First await
    var b = await GetBAsync(); // Second await
    return a + b;
}

// Hot path optimization: Avoid async for simple cases
public Task<int> OptimizedAsync()
{
    // No state machine if method only calls other async methods
    return GetSumAsync();
}

private async Task<int> GetSumAsync()
{
    var aTask = GetAAsync();
    var bTask = GetBAsync();
    
    await Task.WhenAll(aTask, bTask);
    return aTask.Result + bTask.Result;
}

When to Avoid Async

// ❌ Unnecessary async overhead
public async Task<string> GetCachedValueAsync(string key)
{
    if (_cache.TryGetValue(key, out string value))
        return value; // Async state machine for synchronous path
    
    value = await GetFromSourceAsync(key);
    _cache[key] = value;
    return value;
}

// ✅ Optimized: Separate sync and async paths
public Task<string> GetCachedValueOptimizedAsync(string key)
{
    if (_cache.TryGetValue(key, out string value))
        return Task.FromResult(value); // No async overhead
    
    return GetCachedValueInternalAsync(key);
}

private async Task<string> GetCachedValueInternalAsync(string key)
{
    var value = await GetFromSourceAsync(key);
    _cache[key] = value;
    return value;
}

10.26 Async Dispose Patterns

❌ Wrong: Blocking dispose in async context

public class Resource : IDisposable
{
    public void Dispose()
    {
        // ❌ Blocking I/O in dispose
        _stream.Flush(); // May block
        _stream.Dispose();
    }
}

// Usage that causes issues
await using (var resource = new Resource())
{
    await resource.DoSomethingAsync();
} // Dispose() blocks here

✅ Correct: IAsyncDisposable pattern

public class AsyncResource : IAsyncDisposable
{
    private readonly Stream _stream;
    
    public async ValueTask DisposeAsync()
    {
        await _stream.FlushAsync(); // Non-blocking
        await _stream.DisposeAsync();
    }
}

// Usage with await using
await using (var resource = new AsyncResource())
{
    await resource.DoSomethingAsync();
} // DisposeAsync() called asynchronously

Mixed Sync/Async Dispose

public class DualDisposeResource : IDisposable, IAsyncDisposable
{
    private bool _disposed = false;
    
    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
    
    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore();
        Dispose(disposing: false);
        GC.SuppressFinalize(this);
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Dispose synchronous resources
                _syncResource?.Dispose();
            }
            
            // Dispose unmanaged resources
            _disposed = true;
        }
    }
    
    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (!_disposed)
        {
            // Dispose asynchronous resources
            if (_asyncResource != null)
                await _asyncResource.DisposeAsync();
            
            _disposed = true;
        }
    }
}

10.27 Task Completion Source Misuse

❌ Wrong: TCS without proper completion

public Task<int> GetValueAsync()
{
    var tcs = new TaskCompletionSource<int>();
    
    // ❌ Forgetting to complete the task
    _eventSource.OnValueReceived += value =>
    {
        // What if this never fires?
        // Task never completes!
    };
    
    return tcs.Task; // Task may never complete
}

✅ Correct: TCS with timeout and cancellation

public async Task<int> GetValueWithTimeoutAsync(
    CancellationToken cancellationToken, 
    TimeSpan timeout)
{
    var tcs = new TaskCompletionSource<int>();
    
    using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    timeoutCts.CancelAfter(timeout);
    
    using var registration = timeoutCts.Token.Register(() =>
        tcs.TrySetCanceled(timeoutCts.Token));
    
    _eventSource.OnValueReceived += value =>
        tcs.TrySetResult(value);
    
    _eventSource.OnError += error =>
        tcs.TrySetException(new InvalidOperationException(error));
    
    return await tcs.Task;
}

TCS Memory Leak Pattern

// ❌ Memory leak: TCS kept alive by event handler
public class LeakyService
{
    private readonly List<TaskCompletionSource<int>> _pendingTasks = new();
    
    public Task<int> WaitForValueAsync()
    {
        var tcs = new TaskCompletionSource<int>();
        _pendingTasks.Add(tcs); // ❌ Never removed
        
        _eventSource.OnValue += value =>
        {
            tcs.TrySetResult(value);
            // ❌ Should remove from list but doesn't
        };
        
        return tcs.Task;
    }
}

// ✅ Correct: Clean up references
public Task<int> WaitForValueCleanAsync()
{
    var tcs = new TaskCompletionSource<int>();
    var weakReference = new WeakReference<TaskCompletionSource<int>>(tcs);
    
    EventHandler handler = null;
    handler = (sender, value) =>
    {
        if (weakReference.TryGetTarget(out var targetTcs))
        {
            targetTcs.TrySetResult(value);
            _eventSource.OnValue -= handler; // Clean up
        }
    };
    
    _eventSource.OnValue += handler;
    return tcs.Task;
}

10.28 Async Local and Execution Context Pitfalls

❌ AsyncLocal Leakage

private static readonly AsyncLocal<int> _asyncLocalValue = new();

public async Task ProcessAsync()
{
    _asyncLocalValue.Value = 42;
    
    await Task.Delay(100);
    
    // ❌ Value persists across async boundaries
    // May leak to other operations on same thread
    Console.WriteLine(_asyncLocalValue.Value); // Still 42
    
    await Task.Run(() =>
    {
        // ❌ May not propagate correctly
        Console.WriteLine(_asyncLocalValue.Value);
    });
}

✅ Correct AsyncLocal Usage

public class ScopedAsyncLocal<T> : IDisposable
{
    private readonly AsyncLocal<T> _asyncLocal;
    private readonly T _previousValue;
    
    public ScopedAsyncLocal(AsyncLocal<T> asyncLocal, T value)
    {
        _asyncLocal = asyncLocal;
        _previousValue = asyncLocal.Value;
        asyncLocal.Value = value;
    }
    
    public void Dispose()
    {
        _asyncLocal.Value = _previousValue; // Restore previous value
    }
}

// Usage
private static readonly AsyncLocal<string> _correlationId = new();

public async Task ProcessWithScopeAsync()
{
    using (new ScopedAsyncLocal<string>(_correlationId, Guid.NewGuid().ToString()))
    {
        await DoWorkAsync(); // Has correlation ID
    } // Correlation ID automatically cleared
    
    await DoOtherWorkAsync(); // No correlation ID
}

10.29 ValueTask Double Await Protection

The Double Await Problem

public async ValueTask<int> GetValueAsync()
{
    if (_cache.TryGetValue(out int value))
        return value; // Returns ValueTask wrapping int
    
    value = await FetchFromSourceAsync();
    _cache.Set(value);
    return value; // Returns ValueTask wrapping int
}

// ❌ DANGER: Double await
var valueTask = GetValueAsync();
var result1 = await valueTask;
var result2 = await valueTask; // May throw or return wrong value

✅ Safe ValueTask Patterns

// Pattern 1: Convert to Task after first await
public async Task<int> GetValueAsTaskAsync()
{
    if (_cache.TryGetValue(out int value))
        return value;
    
    value = await FetchFromSourceAsync();
    _cache.Set(value);
    return value;
}

// Pattern 2: Document non-reusability
/// <summary>
/// Returns a ValueTask that should be awaited only once.
/// Do not store or await multiple times.
/// </summary>
public ValueTask<int> GetValueOnceAsync()
{
    // Implementation
}

// Pattern 3: Use AsTask extension
var valueTask = GetValueAsync();
var task = valueTask.AsTask(); // Convert to reusable Task
var result1 = await task;
var result2 = await task; // Safe (if you really need this)

10.30 Async Method Builder Customization

Custom Async Method Builders (Advanced)

[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))]
public async ValueTask<T> GetPooledAsync<T>()
{
    // Uses pooled state machines to reduce allocations
    await Task.Delay(1);
    return default(T);
}

// Custom builder implementation (simplified)
public struct PoolingAsyncValueTaskMethodBuilder<T>
{
    private PooledStateMachine<T> _stateMachine;
    
    public static PoolingAsyncValueTaskMethodBuilder<T> Create()
    {
        return new PoolingAsyncValueTaskMethodBuilder<T>
        {
            _stateMachine = StateMachinePool<T>.Rent()
        };
    }
    
    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine
    {
        // Pool-aware state machine startup
    }
    
    // Other builder methods...
}

Chapter 10 Summary

You now understand the most dangerous async mistakes that break APIs in production:

  • Deadlocks
  • Thread starvation
  • Unbounded queues
  • Parallel.ForEach async misuse
  • Async void exceptions
  • Task.Run misuse
  • Misbehaving ValueTasks
  • Fake async I/O
  • Underawaiting & overawaiting
  • Not using CancellationToken
  • Fire-and-forget tasks inside controllers

Mastering these prevents:

  • Outages
  • CPU spikes
  • Memory leaks
  • Broken workflows
  • 500 errors
  • Lost emails/jobs
  • Production incidents

🎉 Chapter 10 (Deep Dive) is complete.

You're now ready for the next major chapter:

👉 Chapter 11: Resilience, Idempotency & Retry Patterns in Async APIs

This chapter will cover:

  • Idempotency keys (Stripe-style)
  • Safe retries in distributed systems
  • Preventing duplicate insertion
  • Detecting retry loops
  • Optimistic concurrency
  • Using Polly advanced patterns (Bulkhead, Timeouts, Fallback)
  • Consistent API resilience design

Would you like me to proceed with Chapter 11 (Deep Dive)?

Post a Comment

Feel free to ask your query...
Cookie Consent
We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.
Oops!
It seems there is something wrong with your internet connection. Please connect to the internet and start browsing again.
AdBlock Detected!
We have detected that you are using adblocking plugin in your browser.
The revenue we earn by the advertisements is used to manage this website, we request you to whitelist our website in your adblocking plugin.
Site is Blocked
Sorry! This site is not available in your country.