
async/await 是编译器生成的状态机机制,通过 MoveNext() 推进、TaskAwaiter 挂起唤醒,不创建线程;其将 async 方法重写为 IAsyncStateMachine 结构体,含 state、提升的局部变量、awaiter 字段及 MoveNext() 入口方法。
async/await 不是线程切换,而是编译器生成的状态机在驱动控制流暂停与恢复——它不创建新线程,也不依赖 Task.Run,核心靠 MoveNext() 推进状态、靠 TaskAwaiter 挂起和唤醒。
你写的这段代码:
public async TaskGetDataAsync() { await Task.Delay(100); return 42; }
在编译后,不会生成线程或后台任务,而是被 C# 编译器(csc)自动转换为一个实现了 IAsyncStateMachine 的私有结构体(Release 模式下通常是 struct,避免堆分配)。这个结构体包含:
state:整型字段,记录当前执行到第几个 await 点(比如 -1=未开始,0=刚过第一个 await,-2=已完成)HttpClient 实例等)都被“提升”为该结构体的字段,以跨暂停点保持上下文awaiter:由 Task.GetAwaiter() 返回的 TaskAwaiter 实例,负责注册回调并检查是否已完成MoveNext():唯一入口方法,按 state 值跳转到对应逻辑块,执行完当前阶段后决定是继续还是挂起await 不是阻塞指令,也不是轮询。它本质是三步操作:
task.GetAwaiter().IsCompleted 快速判断任务是否已同步完成awaiter.OnCompleted(action => { /* 调用 MoveNext() */ }) 注册延续(continuation)Task 给调用方,当前栈帧退出(不压栈、不占线程)例如:await client.GetStringAsync(url) 触发的是底层 I/O 完成端口(Windows)或 epoll(Linux)的非阻塞通知机制,操作系统就绪后才调度回 MoveNext() —— 这中间线程可能早已去处理其他请求了。
默认情况下,await 会捕获当前 SynchronizationContext(如 WinForms 的 UI 上下文、ASP.NET Core 的 AspNetCoreSynchronizationContext),确保恢复时回到原上下文。但多数后台逻辑(如数据访问、计算)并不需要 UI 线程或
特定上下文。
.Result 或 .Wait())ConfigureAwait(false) 显式告诉编译器:“恢复时随便哪个线程池线程都行”,跳过上下文捕获逻辑推荐在所有非 UI、非上下文敏感的异步方法末尾加:
await someTask.ConfigureAwait(false);
以下情况会让 await 表现得像同步调用,却毫无收益:
Completed 的 Task(如 Task.FromResult(1)):直接走 IsCompleted == true 分支,MoveNext() 一路执行到底,没挂起也没调度async void 方法里抛异常:无法被调用方 try/catch 捕获,只能由 SynchronizationContext.UnhandledException 处理,极易静默失败async(如 await Task.Run(() => HeavyCalc())):这属于“假异步”,真正该做的是用 Task.Run 显式卸载到线程池,而非套 async/await 壳子最隐蔽的问题是:状态机本身是值类型(struct),但如果方法里引用了闭包或 this,编译器可能被迫装箱为 class,引发不必要的 GC 压力——调试时可用 dotnet dump analyze 查看堆上是否大量存在 *StateMachine 类型实例。