Asynchronous Programming

20 questions found

How does CancellationToken propagate cancellation requests through a chain of async method calls?

Advanced
A CancellationTokenSource creates a CancellationToken that's passed down through every async method in the call chain; any method (or an awaited library call like HttpClient or Task.Delay) that receives the token can periodically check token.IsCancellationRequested or call token.ThrowIfCancellationRequested(), and calling source.Cancel() propagates the signal to every consumer holding that same token.
async Task ProcessAsync(CancellationToken token) {
  for (int i = 0; i < 1000000; i++) {
    token.ThrowIfCancellationRequested(); // cooperative cancellation check
    await DoWorkAsync(i, token); // token passed further down the chain
  }
}

var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5));
await ProcessAsync(cts.Token);
Real-world example Cancelling a long-running batch operation or API request when a user navigates away or a timeout is reached.

Common follow-ups: What exception type is thrown when ThrowIfCancellationRequested() detects cancellation, and how should it typically be handled?

Exception Handling

How does the compiler transform an async method into a state machine, and why does this matter for understanding performance?

Advanced
The compiler rewrites an async method into a compiler-generated struct or class implementing IAsyncStateMachine, with a numeric state field tracking progress and each 'await' becoming a suspend/resume point — this transformation has real allocation costs (especially for a class-based state machine when the method captures local variables or is a class-based async method), which is why hot-path async methods sometimes prefer ValueTask over Task to reduce allocations.
// Conceptual illustration of what the compiler generates behind the scenes:
// class MethodNameStateMachine : IAsyncStateMachine {
//   int state;
//   TaskAwaiter awaiter;
//   void MoveNext() { /* resumes execution from the last await point */ }
// }
Real-world example Understanding why a hot-path async method with many local variables can have measurably higher allocation overhead than an equivalent synchronous one.

Common follow-ups: Under what conditions does the compiler generate a STRUCT-based state machine instead of a class-based one, and why does that matter for allocations?

Memory & Garbage Collection

What is ValueTask<T> and when should you use it instead of Task<T> for performance?

Advanced
ValueTask<T> is a struct that can represent EITHER a synchronously-already-available result OR an in-progress asynchronous operation, avoiding a heap allocation entirely in the common case where a method often completes synchronously (like a cache hit) — but it comes with important usage restrictions (like not awaiting it twice) that Task<T> doesn't have.
public ValueTask<int> GetCachedValueAsync(string key) {
  if (cache.TryGetValue(key, out int value)) {
    return new ValueTask<int>(value); // synchronous path: zero Task allocation
  }
  return new ValueTask<int>(FetchAndCacheAsync(key)); // genuinely async path
}
Real-world example Optimizing a frequently-called caching layer where most calls hit the cache and complete synchronously, avoiding needless Task allocations.

Common follow-ups: Why is it specifically unsafe to await the same ValueTask<T> instance more than once?

Memory & Garbage Collection

How do IAsyncEnumerable<T> and 'await foreach' let you consume an asynchronous stream of values one at a time?

Advanced
IAsyncEnumerable<T>, produced by an async iterator method using 'yield return', represents a sequence where EACH element may require an asynchronous operation to produce — 'await foreach' consumes this sequence, awaiting each MoveNextAsync() call, letting you process large or slow-arriving data streams (like paginated API results) without loading everything into memory upfront.
async IAsyncEnumerable<string> FetchPagesAsync(string url) {
  string? next = url;
  while (next != null) {
    var (items, nextUrl) = await FetchPageAsync(next);
    foreach (var item in items) yield return item;
    next = nextUrl;
  }
}

await foreach (var item in FetchPagesAsync(startUrl)) {
  Console.WriteLine(item);
}
Real-world example Streaming and processing paginated API results or database query results one page at a time without holding the entire dataset in memory.

Common follow-ups: Can an IAsyncEnumerable<T> sequence also accept a CancellationToken to support cancellation mid-stream?

Iterators & yield return

How do you correctly implement a producer-consumer pattern using System.Threading.Channels for asynchronous data pipelines?

Advanced
Channel<T> provides a thread-safe, fully asynchronous queue with separate Writer and Reader handles — a producer calls writer.WriteAsync() (or TryWrite) to add items, and a consumer uses 'await foreach' over reader.ReadAllAsync() to process them as they arrive, with built-in backpressure support when the channel has a bounded capacity.
var channel = Channel.CreateBounded<int>(capacity: 100);

// Producer
_ = Task.Run(async () => {
  for (int i = 0; i < 1000; i++) await channel.Writer.WriteAsync(i);
  channel.Writer.Complete();
});

// Consumer
await foreach (var item in channel.Reader.ReadAllAsync()) {
  Console.WriteLine(item);
}
Real-world example Building a bounded, backpressure-aware pipeline where a fast producer (like a file reader) shouldn't overwhelm a slower consumer (like a database writer).

Common follow-ups: How does a BOUNDED channel's WriteAsync() behave differently from an unbounded one when the channel is full?

Multithreading & Task Parallel Library

What is the difference between the async and await keywords in C#, and how do they work together?

Beginner
async marks a method as containing asynchronous logic, enabling the use of await inside it, but by itself does not make the method run on a background thread -- await is what actually pauses execution of the async method at that specific point until the awaited Task completes, WITHOUT blocking the calling thread, letting the application remain responsive while the operation (an API call, file read, database query) finishes in the background; once the awaited task completes, execution resumes from exactly where it left off.
public async Task<string> GetDataAsync() {
    await Task.Delay(2000); // simulates a slow I/O operation, without blocking the thread
    return "Data received";
}

public async Task ShowData() {
    string data = await GetDataAsync(); // pauses here until GetDataAsync completes
    Console.WriteLine(data);
}
Real-world example A desktop application fetching weather data from a remote API uses async/await so the UI thread stays responsive (the window can still be dragged/resized) while the three-second network call completes in the background, rather than freezing entirely as a synchronous, blocking call would.

Common follow-ups: What happens if you forget the await keyword in front of an async method call?;Why does calling .Result or .Wait() on a Task risk a deadlock in certain contexts?

Concurrency & Threads;Background Tasks & Hosted Services

What are async streams in C#, and how does IAsyncEnumerable<T> differ from a regular IEnumerable<T> for handling large or slow data sources?

Intermediate
A regular IEnumerable<T> assumes all its elements are already available (or cheaply computable) synchronously; an async stream, defined via IAsyncEnumerable<T> combined with yield return inside an async method, produces elements one at a time asynchronously, letting each element be awaited individually as it becomes available -- consumed via 'await foreach', this lets an application process data that arrives gradually (a paginated API, a large file read line by line, a real-time feed) without blocking and without loading the entire dataset into memory upfront.
async IAsyncEnumerable<int> GenerateNumbersAsync() {
    for (int i = 1; i <= 5; i++) {
        await Task.Delay(1000); // simulating a slow data source
        yield return i;
    }
}

await foreach (var number in GenerateNumbersAsync()) {
    Console.WriteLine(number); // processes each value as it arrives, without blocking
}
Real-world example A service consuming a paginated third-party API wraps the page-fetching logic in an async stream, letting calling code iterate over all records via 'await foreach' as if it were a simple in-memory collection, while pages are actually being fetched lazily and asynchronously behind the scenes.

Common follow-ups: How does await foreach differ from a plain foreach in terms of what happens between iterations?;What's the memory advantage of IAsyncEnumerable<T> over loading an entire large dataset into a List<T> first?

Async Iterators & Streams;I/O & NIO

What is the difference between Task and Thread in C#, and when would you deliberately choose one over the other?

Advanced
A Thread represents a genuine, independent OS-level execution path, giving fine-grained control (priority, explicit lifetime) but consuming real system resources per thread and requiring manual synchronization if shared state is involved -- appropriate for long-running, dedicated background work needing direct control. A Task is a higher-level abstraction over the thread pool, automatically reusing pooled threads rather than creating new ones per operation, and integrates natively with async/await, making it the preferred choice for short-lived, scalable, I/O-bound work like web requests, file access, or API calls.
// Thread: a dedicated, manually managed execution path
Thread thread = new(() => Console.WriteLine("Running in a thread"));
thread.Start();

// Task: uses the thread pool automatically, integrates with async/await
await Task.Run(() => Console.WriteLine("Running in a task"));
Real-world example A web API handling thousands of concurrent requests relies entirely on Task-based async operations backed by the thread pool, since manually spinning up a dedicated Thread per incoming request would exhaust system resources under real production load.

Common follow-ups: Why does the thread pool make Task more scalable than manually created Threads for short-lived work?;In what specific scenario would a dedicated, long-running Thread genuinely be the better choice today?

Concurrency & Threads;Diagnostics & Performance

What is ValueTask<T> in C#, and when does it actually provide a meaningful performance benefit over Task<T>?

Advanced
Task<T> always allocates an object on the heap, even when a method's result is already available synchronously; ValueTask<T> is a struct that can represent an already-completed result without any heap allocation at all, avoiding that overhead specifically in the common case where a value is often available immediately (like a cache hit) rather than genuinely requiring asynchronous work -- the trade-off is that a ValueTask<T> should be awaited only once and never awaited multiple times or accessed concurrently, since it doesn't guarantee the same safe, repeatable semantics a Task<T> does.
// Task<T>: always allocates, even for an already-known result
public async Task<int> GetValueAsync() => await Task.FromResult(42);

// ValueTask<T>: avoids the allocation entirely when the result is already available
public ValueTask<int> GetValueAsync() => new ValueTask<int>(42);

// If genuine async work is sometimes needed, it can still wrap a real Task<T>
public ValueTask<int> GetCachedOrFetchAsync(string key) =>
    _cache.TryGetValue(key, out int value) ? new ValueTask<int>(value) : new ValueTask<int>(FetchAsync(key));
Real-world example A caching layer's GetAsync method returns ValueTask<T> specifically because the overwhelming majority of calls are cache hits resolved synchronously, avoiding a heap allocation on every single cache lookup across a very high-frequency, latency-sensitive code path.

Common follow-ups: What specific bug can occur if a ValueTask<T> is awaited more than once?;In what scenario would using ValueTask<T> actually hurt readability or correctness enough to not be worth the optimization?

Diagnostics & Performance;Concurrency & Threads

How does async/await work under the hood in C#, and what specifically happens when execution reaches an 'await'?

Advanced
The C# compiler transforms an async method into a compiler-generated state machine rather than executing it as ordinary sequential code -- when execution reaches 'await', the method pauses, control returns immediately to the caller, and the remaining logic is captured as a continuation that gets scheduled to run once the awaited task actually completes; critically, async/await does not by itself create a new thread, it just efficiently schedules continuations, with an actual new thread only introduced if code explicitly calls Task.Run(). Common pitfalls include calling .Result or .Wait() (forcing synchronous blocking on an async call, which can cause deadlocks in contexts with a synchronization context like ASP.NET or WPF) and forgetting ConfigureAwait(false) in library code, causing unnecessary context-switching overhead.
public async Task<int> FetchDataAsync() {
    await Task.Delay(1000);
    return 42;
}
// Conceptually compiles to something resembling:
public Task<int> FetchDataAsync() {
    var stateMachine = new FetchDataStateMachine();
    stateMachine.MoveNext(); // runs synchronously until the first await, then returns control
    return stateMachine.Task;
}
Real-world example A deadlock in a WPF application is traced to a UI event handler calling .Result on an async method, blocking the UI thread while the async continuation was itself waiting to resume on that exact same now-blocked UI thread, resolved by using await instead of .Result throughout the call chain.

Common follow-ups: Why specifically does calling .Result from a UI thread risk a deadlock that calling it from a console app's Main method usually doesn't?;What does ConfigureAwait(false) actually change about which thread a continuation resumes on?

Concurrency & Threads;Diagnostics & Performance

Showing 11–20 of 20