Async and concurrency in C#: a practical guide
Practical await, cancellation, and coordination decisions.
This site uses Walter, an AI agent, for research, writing, and maintenance. Editorial and verification process
This guide collects C# and .NET articles about asynchronous and concurrent code: async/await, cancellation, runtime-async work, locks, channels, and common deadlocks and exceptions.
What to read first
Start with the daily pair: async void vs async Task and .Result vs .Wait() vs GetAwaiter().GetResult() vs await, then the deadlock blocking causes and migrating a legacy codebase to async all the way up. ConfigureAwait(false) vs default in .NET 11 answers whether that still matters.
For return types, what ValueTask is and when it’s worth it, IEnumerable vs IAsyncEnumerable vs IQueryable, and returning a Task directly vs an async passthrough cover the signature choices; stuck in sync code, wait on a ValueTask without allocating. For running work in parallel, Parallel.ForEach vs Parallel.ForEachAsync vs Task.WhenAll decides, with inspect all failures from Task.WhenAll as the gotcha. For coordination, lock vs Monitor vs SemaphoreSlim vs System.Threading.Lock and Channels instead of BlockingCollection are the primitives. Below the locks, Volatile.Read vs Volatile.ReadBarrier picks the fence, Thread.Sleep vs Task.Delay vs SpinWait the way to wait, AsyncLocal<T> flow the context rule, and Lazy<T> vs LazyInitializer vs double-checked locking the way to initialize once. For shutdown, propagating a CancellationToken and CancelAfter timeouts are the pair.
For external programs, waiting for a process in C# demonstrates WaitForExit, WaitForExitAsync, exit codes, and timeout handling in a runnable project. Canceling the wait and terminating the child are separate operations.
What’s on this page
The index collects C# and .NET articles about async, concurrency, or threading, including EF Core coordination patterns. It is ordered newest first. For Dart isolates and Flutter asynchronous UI, use the Flutter guide.
Companion pillars: C# 14 features and the .NET 11 tracker.
Index (46 posts)
2026 / 11
- How to initialize interdependent async singletons at startup in ASP.NET Core
Give each singleton a memoized InitializeAsync that awaits its own dependencies, then await them all from IHostedLifecycleService.StartingAsync. Registration order stops mattering, Kestrel does not accept requests until init finishes, and a failure stops the host. Compared against init before app.Run, IHostedService.StartAsync, and a BackgroundService with a readiness check on ASP.NET Core 10.0.10.
- What is AsyncLocal<T>, and why doesn't a value set inside an awaited method flow back to the caller?
AsyncLocal<T> stores a value in the ExecutionContext, which flows down into awaits, Task.Run and continuations but never back up: every async method restores the caller's context when it returns. Verified on .NET 10: eleven cases with real output, the runtime code that does the restore, and three patterns that do get a value back to the caller.
- What happens if you start a thread from a finalizer in .NET?
Starting a thread inside a finalizer works, but the new thread is a normal-priority foreground thread that resurrects the object, outlives GC.WaitForPendingFinalizers, can crash the process, and may touch objects that were already finalized. Blocking on it hangs shutdown. Eight cases reproduced on .NET 10 with real output, plus the safe handoff pattern.
- What Does EF Core Write Back Into an Entity After SaveChangesAsync?
After SaveChangesAsync, EF Core 11 copies store-generated keys, defaults, computed columns and concurrency tokens back into your entities, fixes up foreign keys and navigations, and resets every entry to Unchanged. Here is exactly what gets written, when, what the SQL looks like, and four cases where the value on your entity is not what is in the database.
2026 / 10
- MudBlazor 9.12: Slow ServerData and Autocomplete Searches No Longer Overwrite Newer Results
MudBlazor 9.12.0 fixes a long-standing race in MudAutocomplete, MudDataGrid and MudTable: the result of the search that finished last used to win, not the one started last. Here is how the fix works and what changes for your ServerData and SearchFunc code.
- Lazy<T> vs LazyInitializer.EnsureInitialized vs double-checked locking in C#
Use Lazy<T> by default: it runs the factory once, caches exceptions, and its reads are too cheap to measure after initialization. Use LazyInitializer.EnsureInitialized when you lazily fill many fields or array slots and cannot afford an extra object per value. Hand-written double-checked locking is almost never worth it on .NET 10.
- Thread.Sleep vs Task.Delay vs SpinWait for waiting in C#
Use await Task.Delay for any timed wait in async code, Thread.Sleep only on a dedicated thread you own, and SpinWait only for sub-microsecond waits inside lock-free code. To wait for a condition, use a signal (ManualResetEventSlim, SemaphoreSlim, TaskCompletionSource) instead of any of the three. Measured on .NET 10.0.10.
- How to tail a log file in C# while another process is still writing to it
Open the log with FileShare.ReadWrite | FileShare.Delete, seek to the end, poll for new bytes, decode them with your own UTF-8 Decoder instead of StreamReader, and handle truncation and rotation explicitly. A tested tail -F implementation for .NET 10 and .NET 11.
- How to wait for a ValueTask synchronously in a non-async method without allocating
Check IsCompleted first and read the result through GetAwaiter().GetResult(), which costs 0 bytes. Only fall back to blocking when the ValueTask is still pending, and do that with a cached event and UnsafeOnCompleted instead of AsTask(), which allocates 72 to 208 bytes per call. Measured on .NET 10 and .NET 11 RC1.
- Volatile.Read vs Volatile.ReadBarrier in .NET 10
Volatile.Read is an acquire load of one location. Volatile.ReadBarrier, new in .NET 10, is a fence that gives every earlier read acquire semantics. Use Volatile.Read for flags and published references, and ReadBarrier when you need a batch of plain or non-atomic reads to complete before the next memory access, as in a seqlock.
2026 / 09
- How to atomically append to a PostgreSQL jsonb array with EF Core and Npgsql
Loading an entity, calling List.Add, and saving rewrites the whole jsonb document and silently loses concurrent appends. Push the append into one UPDATE with the jsonb || operator, either through ExecuteSqlAsync or a mapped function inside ExecuteUpdateAsync, and make it idempotent with a @> guard.
- How to guarantee idempotent message processing with EF Core 11 when two app instances consume the same message
The existence check in your handler is not the guard you think it is. Put a unique index on the inbox table, write the marker in the same SaveChanges as the business change, and let the database pick the winner. Plus the ExecuteUpdate trap that quietly undoes all of it.
- Fix: UIKitThreadAccessException from MediaPicker.PickPhotosAsync when selecting multiple photos in .NET MAUI iOS
Picking 2 or more photos on iOS throws UIKitThreadAccessException in MAUI 10.0.100 and 10.0.101. MAUI reads PHPickerResult.ItemProvider after an await, off the main thread. Pin 10.0.90, upgrade to 10.0.110, or use a PHPicker shim.
- How to Create and Extract ZIP Files Asynchronously with the ZipArchive Async APIs in .NET 11
Use ZipFile.CreateFromDirectoryAsync, ExtractToDirectoryAsync, ZipArchive.CreateAsync and ZipArchiveEntry.OpenAsync without hidden synchronous I/O. Measured on .NET 11 RC 1 and .NET 10.0.12, including the .NET 10 bug that still blocks Kestrel.
- How to find the async void handlers causing ANRs in a .NET MAUI Android app
Play Console tells you your ANR rate is over 0.47% and hands you a native stack full of libcoreclr.so frames. Here is how to get from that useless trace to the exact async void event handler that blocked the main thread, using a Looper printer, a SynchronizationContext wrapper that names the state machine, and dotnet-trace over dsrouter.
- How to use pessimistic locking with UPDLOCK and SELECT ... FOR UPDATE in EF Core 11
EF Core 11 still has no lock API. Here is how to take a real row lock with FromSql: WITH (UPDLOCK, ROWLOCK) on SQL Server, FOR UPDATE on PostgreSQL, the subquery trap that silently widens the lock, NOWAIT and SKIP LOCKED, deadlock retries, and what to do when the row does not exist yet.
- Returning a Task directly vs async/await passthrough in a C# repository method: which should you use?
Eliding async/await in a repository passthrough saves about 6 ns and 72 bytes, and costs you a stack frame, try/catch semantics, and safe disposal. Keep return await unless the method is a pure passthrough on a measured hot path.
2026 / 08
- Fix: 23505: duplicate key value violates unique constraint on a concurrent EF Core insert
The check-then-insert in your handler is not atomic. Catch PostgresException with SqlState 23505, or collapse the whole thing into one INSERT ... ON CONFLICT statement. EnableRetryOnFailure will not help.
- Fix: AggregateException "One or more errors occurred" when awaiting Task.WhenAll in C#
await Task.WhenAll rethrows only one of the failures. Keep the WhenAll task in a variable and read its Exception.InnerExceptions to see every error instead of one.
- Fix: CS1998 "This async method lacks 'await' operators and will run synchronously" in C#
CS1998 means an async method has no await, so it runs synchronously. Drop the async modifier and return Task.FromResult, or add the await you forgot.
- How to implement optimistic concurrency with a rowversion token in EF Core 11
Add a rowversion concurrency token in EF Core 11: the [Timestamp] and IsRowVersion setup, the SQL EF actually emits, catching DbUpdateConcurrencyException, store-wins vs client-wins vs merge, disconnected APIs with ETags, and the five traps that silently disable the whole thing.
2026 / 07
- How to test time-dependent code with TimeProvider and FakeTimeProvider in .NET 11
Replace DateTime.UtcNow, Stopwatch, and Task.Delay with System.TimeProvider so tests can control the clock: DI registration, FakeTimeProvider.Advance and SetUtcNow, testing timeouts and PeriodicTimer-based BackgroundServices, plus the Advance-continuation and xUnit v2 gotchas.
- Migrate from blocking .Result/.Wait() calls to async all the way up in a legacy C# codebase
A staged playbook for removing sync-over-async from an existing .NET codebase: inventory with analyzers, measure ThreadPool starvation, convert one call chain at a time, and ratchet the count to zero on .NET 11.
- .Result vs .Wait() vs GetAwaiter().GetResult() vs await in C#: which should you use?
await is the right answer almost every time. When you truly must block, GetAwaiter().GetResult() beats .Result and .Wait() because it throws the original exception. A decision matrix for .NET 11 and C# 14.
- Fix: CS4014 "Because this call is not awaited, execution of the current method continues" in C#
CS4014 means you called a Task-returning method without awaiting it. Add await, or discard with _ = if fire-and-forget is truly intended, and handle exceptions.
- Fix: deadlock when calling .Result or .Wait() on an async method in C#
Blocking on an async Task with .Result or .Wait() deadlocks when a SynchronizationContext is present. Here is why it hangs and how to fix it in .NET 11 and C# 14.
- .NET 11 Runtime Async Drops the EnablePreviewFeatures Flag
As the .NET 11 previews progress toward the November release, Runtime Async has graduated: net11.0 projects opt in with a single MSBuild property, and the runtime libraries themselves now ship compiled on it.
- How to implement and consume IAsyncDisposable with await using in C#
A complete guide to IAsyncDisposable in C#: when to use await using, how to write DisposeAsync and DisposeAsyncCore correctly, and the stacking and ConfigureAwait gotchas that leak resources.
- How to propagate a CancellationToken through async methods in .NET 11
Thread a CancellationToken cleanly through every layer of an async call chain in .NET 11: last-parameter convention, default values, linked tokens, ASP.NET Core RequestAborted, and the CA2016 analyzer that catches the ones you drop.
- How to time out an async operation with CancellationTokenSource.CancelAfter in C#
Use CancellationTokenSource.CancelAfter to enforce an async deadline in .NET 11: constructor vs CancelAfter, linked tokens for composing timeouts with caller tokens, exception disambiguation, Task.WaitAsync, TryReset for pooling, and testable timeouts with TimeProvider.
2026 / 06
- What is ValueTask<T> and when is it worth it?
ValueTask and ValueTask<T> are structs that let an async method return a result without heap-allocating a Task when it completes synchronously. The win is one fewer allocation on hot paths that usually finish without awaiting. The cost is a strict await-once contract. Here is what the type actually is, how it works, and the narrow set of cases where it earns its keep.
- What is IAsyncEnumerable<T> and when should I use it?
IAsyncEnumerable<T> is the interface for asynchronous streams: a sequence whose elements arrive over time and each one may require an await. Here is what it actually is, how await foreach and yield drive it, and the rule for when to reach for it over Task<List<T>>.
- Migrate from ValueTask<T> back to Task<T>: when and why (.NET 11, C# 14)
A practical checklist for reverting ValueTask and ValueTask<T> return types to Task and Task<T>, what breaks at the call sites, how to verify each change, and how to know whether the swap was ever worth it.
- Fix: ObjectDisposedException: Cannot access a disposed context instance
Your fire-and-forget task captured a request-scoped DbContext that the DI scope already disposed. Resolve a fresh context inside the task with IServiceScopeFactory or IDbContextFactory.
2026 / 05
- How to run fire-and-forget work safely in ASP.NET Core with BackgroundService
Calling Task.Run from a controller loses work on shutdown, swallows exceptions, and captures disposed scoped services. The safe pattern is a bounded Channel queue drained by a BackgroundService that opens a fresh scope per work item and drains in-flight work on StopAsync.
- Parallel.ForEach vs Parallel.ForEachAsync vs Task.WhenAll in C#
Use Parallel.ForEach for CPU-bound work over in-memory data, Parallel.ForEachAsync for async I/O over many items with a concurrency cap, and Task.WhenAll for a small fixed fan-out where you want every operation in flight and need the results back.
- lock vs Monitor vs SemaphoreSlim vs System.Threading.Lock in C#
Four ways to guard a critical section in C#, and a decision matrix for picking one. Use System.Threading.Lock for synchronous mutual exclusion on .NET 9+, SemaphoreSlim when the section spans an await, and Monitor only when you need Wait/Pulse.
- Task.Run vs Task.Factory.StartNew vs ThreadPool.QueueUserWorkItem
Three ways to push work onto the thread pool in C#, and which one to reach for. Use Task.Run for almost everything, ThreadPool.QueueUserWorkItem<TState> for allocation-free fire-and-forget, and Task.Factory.StartNew only for LongRunning or a custom scheduler.
- ConfigureAwait(false) vs default in .NET 11: does it still matter?
ConfigureAwait(false) is still mandatory in library code that may run under a SynchronizationContext (WinForms, WPF, MAUI). In application code on ASP.NET Core, a console app, or a worker service running on .NET 11, it is a no-op.
- IEnumerable vs IAsyncEnumerable vs IQueryable in C#: which one should the method return?
Three sequence interfaces, three execution models. Use IQueryable when a database can translate the query, IAsyncEnumerable when the producer is async and you want to stream, IEnumerable for everything else in memory.
- async void vs async Task in C#: when each is correct
async Task is the default and async void is the exception. Use async void only for event handlers, top-level message-loop handlers, and a handful of framework callbacks that demand a void signature. Everywhere else, async Task wins on exceptions, composition, and testability.
- Fix: A second operation was started on this context instance before a previous operation completed
EF Core throws when two awaits run in parallel on the same DbContext. Await each call sequentially, or get a new DbContext per concurrent unit of work via IDbContextFactory.
2026 / 04
- How to use System.Threading.Lock in .NET
Use System.Threading.Lock for synchronous critical sections on .NET 9 and later. Understand compiler lowering, async restrictions and Monitor compatibility.
- How to use Channels instead of BlockingCollection in C#
System.Threading.Channels is the async-first replacement for BlockingCollection in .NET 11. This guide shows how to migrate, how to choose bounded vs unbounded, and how to handle backpressure, cancellation, and graceful shutdown without deadlocking.
- How to cancel a long-running Task in C# without deadlocking
Cooperative cancellation with CancellationToken, CancelAsync, Task.WaitAsync, and linked tokens in .NET 11. Plus the blocking patterns that turn a clean cancel into a deadlock.
- .NET 11 Runtime Async Replaces State Machines with Cleaner Stack Traces
Runtime Async in .NET 11 moves async/await handling from compiler-generated state machines into the runtime itself, producing readable stack traces, correct breakpoints, and fewer heap allocations.