为何异步调用回调函数可避免Stack Dive(栈潜入)问题?
为什么异步调用回调能避免栈潜入(Stack Dive)问题?
我在阅读Stephen Toub的博客文章《How Async/Await Really Works in C#》时,看到一段代码会引发栈溢出——原因是BeginRead方法同步完成后会直接调用AsyncCallback Lambda表达式,进而再次调用BeginRead,循环往复导致栈帧不断累积,最终触发栈溢出。
这段引发问题的代码如下:
using System.Net; using System.Net.Sockets; using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); listener.Listen(); using Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); client.Connect(listener.LocalEndPoint!); using Socket server = listener.Accept(); _ = server.SendAsync(new byte[100_000]); var mres = new ManualResetEventSlim(); byte[] buffer = new byte[1]; var stream = new NetworkStream(client); void ReadAgain() { stream.BeginRead(buffer, 0, 1, iar => { if (stream.EndRead(iar) != 0) { ReadAgain(); // uh oh! } else { mres.Set(); } }, null); }; ReadAgain(); mres.Wait();
文中给出的解决方案之一是:
不允许同步调用AsyncCallback。即使操作同步完成,也始终异步调用回调函数,这样就能消除栈潜入的风险。但这也会影响性能,因为同步完成(或快到难以区分)的操作非常常见,强制每个此类操作将回调加入队列会带来可观的开销。
对此我有个疑问:为什么异步调用回调函数就能避免栈潜入问题?
核心原因:栈帧的释放时机差异
同步调用回调导致栈溢出的逻辑
当BeginRead同步完成时,如果直接同步执行回调,整个过程是在同一个调用栈里连续执行的:
- 第一次调用
ReadAgain(),栈中压入ReadAgain的栈帧 BeginRead同步完成,立刻执行Lambda回调,栈中再压入这个Lambda的栈帧- 回调里又调用
ReadAgain(),栈中再次压入新的ReadAgain栈帧 - 循环往复,每一轮都会往栈里新增栈帧,栈空间被持续占用,直到耗尽触发栈溢出。
异步调用回调避免栈潜入的逻辑
如果强制异步调用回调,执行流程会被打断,栈帧能及时释放:
BeginRead同步完成后,不会立刻执行回调,而是把回调任务放到线程池(或其他异步调度队列)中等待执行- 当前的
ReadAgain()方法执行完毕,对应的栈帧会被立即释放 - 后续线程池取出回调任务执行时,是在全新的调用栈上下文中运行,回调里再调用
ReadAgain(),也只会在这个新栈中压入帧,执行完毕后栈帧会被释放,不会出现栈帧持续累积的情况。
简单来说,异步调用把原本“栈上连续递归”的逻辑,变成了“队列调度的分散执行”,每次循环都能清空之前的栈空间,自然就不会出现栈潜入导致的溢出问题。
内容的提问来源于stack exchange,提问作者Nathanial Boehlje
相关产品推荐
相关产品推荐

