| name | async |
| description | Async/await and concurrency patterns |
Cleary: Async Done Right
Stephen Cleary's core belief: Async is about scalability, not performance. It frees threads to do other work while waiting. Get the patterns right, and async code is as simple as sync code.
The Foundational Principle
"Async all the way down. Don't block on async code."
The moment you block (.Result, .Wait()), you lose all benefits and risk deadlocks.
Core Principles
1. Async All the Way
Async must propagate through the entire call stack.
Not this:
public string GetData()
{
return GetDataAsync().Result;
}
public string GetData()
{
return GetDataAsync().GetAwaiter().GetResult();
}
This:
public async Task<string> GetDataAsync()
{
return await httpClient.GetStringAsync(url);
}
public async Task ProcessAsync()
{
var data = await GetDataAsync();
}
Why blocking deadlocks:
In UI/ASP.NET contexts, await captures a synchronization context. When you block with .Result, the thread waits. When the async operation completes, it tries to resume on that same thread—which is blocked waiting. Deadlock.
2. Avoid async void
async void is fire-and-forget with no error handling.
Not this:
async void ProcessData()
{
await Task.Delay(1000);
throw new Exception("Oops");
}
This:
async Task ProcessDataAsync()
{
await Task.Delay(1000);
throw new Exception("Oops");
}
The one exception: Event handlers must be async void:
private async void Button_Click(object sender, EventArgs e)
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
ShowError(ex.Message);
}
}
3. Use CancellationToken
Every async operation should be cancellable.
public async Task<Data> FetchDataAsync(CancellationToken cancellationToken = default)
{
var response = await httpClient.GetAsync(url, cancellationToken);
foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessItemAsync(item, cancellationToken);
}
return result;
}
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
var data = await FetchDataAsync(cts.Token);
}
catch (OperationCanceledException)
{
}
Cancellation patterns:
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var cts = new CancellationTokenSource();
cts.Cancel();
var linked = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);
cancellationToken.Register(() => CleanupResources());
4. ConfigureAwait(false) in Libraries
Library code shouldn't capture the synchronization context.
public async Task UpdateUIAsync()
{
var data = await GetDataAsync();
label.Text = data;
}
public async Task<string> GetDataAsync()
{
var response = await httpClient.GetAsync(url)
.ConfigureAwait(false);
return await response.Content.ReadAsStringAsync()
.ConfigureAwait(false);
}
Rule of thumb:
- Application code: Omit ConfigureAwait (keep context)
- Library code: Always
.ConfigureAwait(false)
5. Prefer Task.WhenAll for Concurrency
Don't await in loops when operations are independent.
Not this:
var results = new List<Data>();
foreach (var id in ids)
{
var data = await FetchAsync(id);
results.Add(data);
}
This:
var tasks = ids.Select(id => FetchAsync(id));
var results = await Task.WhenAll(tasks);
Error handling with WhenAll:
var tasks = ids.Select(id => FetchAsync(id)).ToList();
try
{
var results = await Task.WhenAll(tasks);
}
catch
{
var exceptions = tasks
.Where(t => t.IsFaulted)
.Select(t => t.Exception);
}
6. Use ValueTask for Hot Paths
When methods often complete synchronously, avoid Task allocation.
public async Task<int> GetValueAsync()
{
if (cache.TryGetValue(key, out var value))
return value;
return await FetchFromDatabaseAsync();
}
public async ValueTask<int> GetValueAsync()
{
if (cache.TryGetValue(key, out var value))
return value;
return await FetchFromDatabaseAsync();
}
ValueTask rules:
- Don't await multiple times
- Don't use .Result/.Wait()
- Don't use with Task.WhenAll (convert to Task first)
- Only use when sync completion is common
7. Avoid Task.Run in ASP.NET
ASP.NET is already async. Task.Run just moves work to another thread.
Not this:
public async Task<IActionResult> GetData()
{
var data = await Task.Run(() => ProcessData());
return Ok(data);
}
This:
public IActionResult GetData()
{
var data = ProcessData();
return Ok(data);
}
public async Task<IActionResult> GetData()
{
var data = await ProcessDataAsync();
return Ok(data);
}
When Task.Run is appropriate:
- UI applications: Keep UI responsive during CPU work
- Wrapping sync I/O that can't be made truly async
Common Patterns
Async Initialization
public class Service
{
private readonly AsyncLazy<Database> _db;
public Service()
{
_db = new AsyncLazy<Database>(() => InitializeDatabaseAsync());
}
public async Task<Data> GetDataAsync()
{
var db = await _db;
return await db.QueryAsync();
}
}
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<Task<T>> factory)
: base(() => Task.Run(factory)) { }
public TaskAwaiter<T> GetAwaiter() => Value.GetAwaiter();
}
Async Disposal
public class Connection : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await CloseAsync();
GC.SuppressFinalize(this);
}
}
await using var connection = new Connection();
await connection.SendAsync(data);
Async Streams
public async IAsyncEnumerable<Item> GetItemsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var batch in GetBatchesAsync(cancellationToken))
{
foreach (var item in batch)
{
yield return item;
}
}
}
await foreach (var item in GetItemsAsync(cancellationToken))
{
await ProcessAsync(item);
}
Semaphore for Throttling
private readonly SemaphoreSlim _semaphore = new(maxConcurrency: 10);
public async Task<Data> FetchWithThrottleAsync(string url)
{
await _semaphore.WaitAsync();
try
{
return await httpClient.GetAsync(url);
}
finally
{
_semaphore.Release();
}
}
Anti-Patterns to Avoid
Sync Over Async
public Data GetData()
{
return GetDataAsync().Result;
return GetDataAsync().GetAwaiter().GetResult();
Task.Run(() => GetDataAsync()).Result;
}
Async Over Sync
public Task<Data> GetDataAsync()
{
return Task.FromResult(ComputeDataSync());
}
public Task<Data> GetDataAsync()
{
return Task.Run(() => ComputeDataSync());
}
Fire and Forget Without Handling
_ = DoSomethingAsync();
_ = DoSomethingAsync().ContinueWith(t =>
{
if (t.IsFaulted)
logger.LogError(t.Exception);
});
BackgroundTask.Run(async () =>
{
await DoSomethingAsync();
});
The Cleary Test
Before committing async code, ask:
- Async all the way? No
.Result, .Wait(), or .GetAwaiter().GetResult()?
- No async void? Except for event handlers?
- CancellationToken passed? Through the entire call chain?
- ConfigureAwait(false)? In library code?
- Concurrent where possible? Using WhenAll for independent operations?
- ValueTask appropriate? For hot paths with sync completion?
When to Apply
| Scenario | Apply Cleary |
|---|
| Any async/await code | Yes |
| Cancellation patterns | Yes |
| Concurrent operations | Yes |
| Deadlock debugging | Yes |
| Thread synchronization (locks) | Partially - async locks differ |
| Parallel CPU-bound work | No - use TPL/PLINQ |
Source Material
- "Concurrency in C# Cookbook" (2nd Edition, O'Reilly, 2019)
- Blog: blog.stephencleary.com
- AsyncEx library (authored by Cleary)
- Microsoft async guidance contributions
"There is no thread." — Stephen Cleary (on understanding async)