How it works
Calling an async method begins executing its body immediately. It does not automatically place the whole method on a background thread. Work prints start before the caller prints caller, because the method reaches its first incomplete await only after writing that initial line.
The TaskCompletionSource creates an explicit gate whose task is not complete yet. Awaiting it lets Work suspend and return a task representing its eventual completion. The caller then prints its own line, opens the gate, and awaits work. Only after the gate is opened can Work print end.
An await of an already completed task may continue without suspending. This is why counting await keywords is not a reliable way to predict scheduling. Ask whether the awaited operation is complete at that moment, which context resumes the continuation, and which ordering relationships the program establishes explicitly.
The gate makes this example deterministic without depending on timer precision or a race between threads. RunContinuationsAsynchronously prevents the setter from directly running ordinary registered continuations inline as part of completion. The important output order still comes from the gate's completion dependency, not from a guessed delay duration.
Practice by completing gate before calling Work. Predict the order of all three output lines, then run the program. Next, move the caller's print after await work and explain why the caller now waits for the entire operation rather than merely starting it.
Finally, inspect a real async method's synchronous prefix. Expensive CPU work placed before an incomplete await can still block its caller's thread. Async is a composition mechanism for completion and suspension; choosing where CPU work executes is a separate decision, constrained by the environment. In the browser runtime, do not assume a desktop thread pool is available.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
var gate = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
Task work = Work();
Console.WriteLine("caller");
gate.SetResult(true);
await work;
async Task Work() {
Console.WriteLine("start");
await gate.Task;
Console.WriteLine("end");
}
Expected output
start caller end
The method runs immediately until it reaches an await that cannot complete yet.