C#异步调用链中Async-Await的堆分配问题确认
先把你提供的示例代码格式化一下,方便分析:
static async Task OneAsync() { Console.WriteLine("OneAsync: Start"); await TwoAsync(); Console.WriteLine("OneAsync: End"); } static async Task TwoAsync() { Console.WriteLine("TwoAsync: Start"); await ThreeAsync(); Console.WriteLine("TwoAsync: End"); } static async Task ThreeAsync() { Console.WriteLine("ThreeAsync: Start"); var c = new HttpClient(); var content = await c.GetStringAsync("http://google.com"); Console.WriteLine("Content:" + content.Substring(0, 10)); Console.WriteLine("ThreeAsync: End"); }
咱们逐一验证你的两个假设:
假设1:调用OneAsync时,三个AsyncStateMachine都会被分配到堆上?
结论:是的,三个状态机都会被装箱到堆上。
原因很直白:当async方法执行到第一个未完成的await时,当前方法的执行需要暂停,后续要在异步操作完成后恢复执行。这时候编译器生成的AsyncStateMachine结构体需要被持久化(比如被异步操作的回调引用),而结构体是值类型,要被持久化就必须被装箱到堆上。
在你的示例里,ThreeAsync中的GetStringAsync是真正的异步IO操作,会返回一个未完成的Task,所以ThreeAsync的状态机会被装箱;接着TwoAsync等待这个未完成的Task,它自己的执行也需要暂停,状态机同样被装箱;最后OneAsync等待TwoAsync返回的未完成Task,它的状态机也会被装箱到堆上。
假设2:若ThreeAsync直接返回Task.CompletedTask,此时仅存在2个AsyncStateMachine,且无堆分配?
结论:状态机依然是三个,但确实不会产生堆分配。
首先要纠正一点:只要是async方法,不管它有没有实际的异步逻辑,编译器都会为它生成对应的AsyncStateMachine结构体——哪怕ThreeAsync改成下面这样,依然会有它专属的状态机:
static async Task ThreeAsync() { Console.WriteLine("ThreeAsync: Start"); await Task.CompletedTask; Console.WriteLine("ThreeAsync: End"); }
不过重点是堆分配的问题:从C# 7.0开始,编译器有一个同步完成优化——当async方法可以全程同步执行(所有await的都是已完成的Task)时,编译器不会将状态机装箱到堆上。状态机实例只会在栈上临时创建,方法执行完成后就被销毁,不会产生堆内存分配。
在这个修改后的场景里,整个调用链的所有await都是等待已完成的Task,所以三个状态机都只会在栈上存在,不会有堆分配的情况。
内容的提问来源于stack exchange,提问作者aspnetuser

