.NET Framework 4.5 ASP.NET应用冻结问题分析:基于.NET Core源码的分析是否适用及相关技术疑问
问题背景
我们有一个部署在内部IIS服务器上的.NET Framework 4.5版本ASP.NET应用程序,近期发现应用在重启一段时间后出现冻结无响应的情况。抓取内存转储文件并使用WinDbg工具分析后,发现程序卡在了ExecutionManager::FindCodeRangeWithLock方法处。由于.NET Framework 4.5并非开源,我们参考了GitHub上dotnet/runtime仓库中的C++方法实现,发现该方法在ReaderLockHolder构造函数中通过EE_LOCK_TAKEN(GetPtrForLockContract())持有锁;同时查看了EE_LOCK_TAKEN的定义,它接收枚举值kDbgStateLockType_EE,根据注释说明:
EE locks (used to sync EE structures). These do not include CRST_HOST_BREAKABLE Crsts, and are thus not held while managed code runs
该锁理论上不应在托管代码运行时被持有,但WinDbg截图显示该线程已运行超过40分钟,且最后一帧是在C#库的catch块中执行throw;语句导致的重新抛出异常。使用sosex命令!mk查看的线程栈显示,托管代码抛出了Microsoft.Ajax.Utilities.RecoveryTokenException,该异常在后续帧被捕获并重新抛出。
技术疑问解答
1. 基于.NET Core源码进行的分析是否适用于.NET Framework 4.5?
答案是仅能作为参考,不能完全等同。虽然dotnet/runtime仓库包含了.NET Core、.NET 5+的运行时源码,且这些版本是从.NET Framework的代码基础上重构而来,但.NET Framework 4.5的运行时核心组件(比如CLR的Execution Engine)并没有完全开源,且在后续的.NET Core重构中,很多内部逻辑(包括锁机制、代码范围查找的路径)都有了大幅调整。
举个例子,ExecutionManager::FindCodeRangeWithLock的锁逻辑在.NET 4.5中可能存在和Core版本不同的分支处理,或者锁的粒度、持有时机的设计有差异。所以你基于Core源码的分析可以帮你理解大致的逻辑走向,但不能直接套用到4.5的场景下,具体问题还需要结合4.5的CLR行为来推断。
2. 如果上述分析适用,为何通常在catch块中抛出异常时不会出现阻塞情况?
正常情况下,throw;在catch块中只是重新抛出异常,不会触发长时间的锁阻塞——因为这个操作的核心是栈展开和异常传递,一般不会涉及到需要持有EE锁的运行时内部操作。但你的场景特殊在:
- 异常类型是
Microsoft.Ajax.Utilities.RecoveryTokenException,这个异常来自ASP.NET AJAX的工具库,可能和动态代码生成/编译有关(比如AJAX的脚本压缩、动态生成的代码块)。 - 当运行时处理这个异常的栈展开时,可能需要查找动态生成代码的代码范围信息,而这个路径正好触发了
FindCodeRangeWithLock方法,需要获取EE锁。 - 阻塞的根本原因不是
throw;本身,而是EE锁的竞争或死锁:要么这个锁被其他线程长时间持有(比如另一个线程在持有EE锁时发生了阻塞),要么当前线程在获取锁的路径中遇到了异常导致锁无法正常释放,最终导致该线程长时间等待锁。
简单来说,throw;只是触发问题的导火索,真正的问题是EE锁的异常持有/竞争,而这个场景刚好在异常处理的路径中被触发了。
3. 从GitHub仓库的代码来看,EECodeInfo::Init调用的是FindCodeRange,为何实际会调用FindCodeRangeWithLock?
在dotnet/runtime的Core版本代码中,EECodeInfo::Init调用FindCodeRange是有条件的——通常是在已经持有合适锁的前提下,直接调用无锁版本;如果当前上下文没有持有锁,FindCodeRange内部会自动调用带锁的FindCodeRangeWithLock来保证线程安全。
回到.NET Framework 4.5的场景,很可能存在类似的逻辑:当EECodeInfo::Init在异常处理的路径中被调用时,当前线程并没有预先持有EE锁,所以运行时会自动进入带锁的分支,调用FindCodeRangeWithLock来获取锁并执行代码范围查找。另外,4.5的CLR实现中,EECodeInfo::Init的逻辑可能和Core版本有差异,比如针对动态生成代码的场景,会直接调用带锁的版本来避免并发问题。
内容的提问来源于stack exchange,提问作者Brian Ding




