Menu
CoddyTech

C# Async and Await Interview Questions

Tasks, threads, async and await, cancellation, deadlocks and thread safety.

10 questions
  1. What do async and await do in C#?

    async lets a method use await; await pauses the method until a Task completes without blocking the thread, and the rest runs later as a continuation.

    C#
    using System;
    using System.Threading.Tasks;
    
    class Program
    {
        static async Task<int> DownloadSizeAsync(string url)
        {
            await Task.Delay(100);          // stands in for a network call
            return url.Length * 100;
        }
    
        static async Task RunAsync()
        {
            int size = await DownloadSizeAsync("coddy.tech");
            Console.WriteLine("size: " + size);
        }
    
        static void Main()
        {
            RunAsync().Wait();              // C# 7.1 and later allow async Task Main instead
        }
    }

    No thread is busy while the method waits on I/O, which is why async lets a server handle more requests. See async and await.

  2. In what order does the code around an await run?

    C#
    using System;
    using System.Threading.Tasks;
    
    class Program
    {
        static async Task Work()
        {
            Console.Write("A");
            await Task.Delay(50);
            Console.Write("C");
        }
    
        static void Main()
        {
            Task t = Work();
            Console.Write("B");
            t.Wait();
            Console.WriteLine();
        }
    }

    Work() runs synchronously until its first await on an unfinished task, so A prints inside the call. It then returns a pending Task; Main prints B and blocks on t.Wait(), and the continuation prints C. Code before the first await runs on the caller's thread.

  3. What is the difference between a Task and a Thread?

    A Thread is an OS thread you manage; a Task is an operation and its result, which may use no thread at all. Task.Run work runs on a thread pool thread, and I/O tasks hold no thread while they wait. Tasks also return values, rethrow exceptions on await, and compose with WhenAll.

  4. What is the difference between Task.WhenAll, Task.WaitAll and Task.WhenAny?

    WhenAll returns a task that completes when every task completes; WaitAll blocks the thread until they finish; WhenAny completes with the first task to finish.

    C#
    using System;
    using System.Diagnostics;
    using System.Threading.Tasks;
    
    class Program
    {
        static async Task<int> Fetch(int id, int ms)
        {
            await Task.Delay(ms);
            return id * 10;
        }
    
        static async Task RunAsync()
        {
            var sw = Stopwatch.StartNew();
            int[] results = await Task.WhenAll(Fetch(1, 300), Fetch(2, 300), Fetch(3, 300));
            Console.WriteLine(string.Join(",", results));
            Console.WriteLine(sw.ElapsedMilliseconds < 600 ? "ran concurrently" : "ran one by one");
    
            Task<int> first = await Task.WhenAny(Fetch(4, 400), Fetch(5, 50));
            Console.WriteLine("first result: " + first.Result);
        }
    
        static void Main() { RunAsync().Wait(); }
    }

    Three 300 ms calls take about 300 ms. Trap: awaiting WhenAll rethrows only the first exception, while the WhenAll task's Exception.InnerExceptions holds all of them.

  5. Why should you avoid async void?

    An async void method cannot be awaited, so the caller cannot tell when it finishes or catch its exceptions, which usually crash the process.

    async void SaveBad()  { await db.SaveAsync(); }   // fire and forget, exceptions escape
    async Task SaveGood() { await db.SaveAsync(); }   // caller can await and catch
    
    try { SaveBad(); } catch { /* never runs for async exceptions */ }
    

    Use it only for event handlers, with a try and catch inside. An async lambda passed as an Action is also async void.

  6. What is ValueTask, and when should you use it instead of Task?

    ValueTask<T> is a struct that can carry a result directly, so a method that usually completes synchronously (a cache hit) avoids allocating a Task. Use it on such hot paths and Task elsewhere.

    private readonly Dictionary<int, string> cache = new();
    
    public ValueTask<string> GetNameAsync(int id)
    {
        if (cache.TryGetValue(id, out var name))
            return new ValueTask<string>(name);                // no allocation
        return new ValueTask<string>(LoadAndCacheAsync(id));   // wraps a real Task
    }
    

    Await a ValueTask exactly once, and never read .Result before it completes.

  7. Why can .Result or .Wait() on an async method deadlock, and what does ConfigureAwait(false) change?

    With a single-threaded synchronization context (WinForms, WPF, classic ASP.NET), .Result blocks the thread the awaited method needs to resume on, so each waits for the other.

    // WPF button handler
    var data = LoadAsync().Result;   // blocks the UI thread
    
    async Task<string> LoadAsync()
    {
        var s = await http.GetStringAsync(url);  // wants to resume on the UI thread
        return s;                                // never gets there
    }
    

    Await all the way. In library code, ConfigureAwait(false) resumes on the thread pool instead of the captured context. ASP.NET Core has no synchronization context, so it does not deadlock this way.

  8. How do you cancel an async operation in C#?

    Pass a CancellationToken down the call chain; the caller cancels through its CancellationTokenSource, and the code passes the token on or calls ThrowIfCancellationRequested().

    C#
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    class Program
    {
        static async Task<int> SlowWork(CancellationToken token)
        {
            int steps = 0;
            for (int i = 0; i < 100; i++)
            {
                token.ThrowIfCancellationRequested();
                await Task.Delay(20, token);
                steps++;
            }
            return steps;
        }
    
        static async Task RunAsync()
        {
            var cts = new CancellationTokenSource();
            cts.CancelAfter(100);
            try
            {
                await SlowWork(cts.Token);
                Console.WriteLine("finished");
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine("canceled");
            }
        }
    
        static void Main() { RunAsync().Wait(); }
    }

    Cancellation is cooperative: code that never checks the token cannot be stopped.

  9. What does the lock statement do, and when do you use Interlocked instead?

    lock (obj) lets one thread at a time run the block for that object, through Monitor.Enter and Monitor.Exit in a try and finally (on a C# 13 System.Threading.Lock, through EnterScope(); see the lock statement docs). For a single counter, Interlocked.Increment is a single atomic operation.

    C#
    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    class Program
    {
        static int unsafeCount, lockedCount, atomicCount;
        static readonly object gate = new object();
    
        static void Main()
        {
            Parallel.For(0, 100000, i =>
            {
                unsafeCount++;                          // race: read, add, write
                lock (gate) { lockedCount++; }
                Interlocked.Increment(ref atomicCount);
            });
    
            Console.WriteLine("locked: " + lockedCount);
            Console.WriteLine("atomic: " + atomicCount);
            Console.WriteLine("unsafe is exact: " + (unsafeCount == 100000));
        }
    }

    The unsynchronized counter usually loses updates, so its line can print False or True. You cannot await inside a lock; use SemaphoreSlim.WaitAsync. See lock.

  10. How does async/await work under the hood?

    The compiler rewrites an async method into a state machine: locals that live across an await become fields, and each await becomes a numbered state. At an await, if the task is already complete the method carries on synchronously; otherwise it registers MoveNext as the continuation and returns a pending Task to the caller.

    When the task finishes, MoveNext resumes at the saved state, on the captured SynchronizationContext if there is one, otherwise usually on a thread pool thread. No thread waits during the await, and any type with a suitable GetAwaiter() method can be awaited.

Coddy programming languages illustration

Learn to code with Coddy

Get started