C# Winforms应用程序挂起而不是抛出异常

3
昨天我遇到了一个非常奇怪的错误,一天过去了,我几乎没有取得任何进展,所以我认为这是向社区寻求帮助的好机会。我想要请求一些耐心,因为我认为这是一个比较棘手的问题。
我有一个C# Winforms应用程序,在生产环境中点击几次后就会挂起。在开发环境中从未出现过这种情况,只有在生产环境中出现。当挂起发生时,没有什么真正的反应(没有错误消息,但任务管理器显示任务处于“无响应”状态),但GUI变得不响应。我在同样的环境下尝试了它,可以确认这种行为。
不幸的是,在生产环境中无法安装开发工具并调试应用程序。我能做的最好的事情就是在应用程序停止时制作内存转储。问题是我完全不理解在崩溃转储中看到的内容:我的主线程(GUI线程)似乎卡在一个我找不到任何原因的指令上。
以下是我的主线程的堆栈跟踪:
KERNELBASE.dll!_RaiseException@16()  + 0x54 bytes    
[External Code]    
CFAPControlLibrary.dll!CFAPControlLibrary.Communication.Base.GetSetting(string settingName) Line 850 + 0x10 bytes    C#
CFAPControlLibrary.dll!CFAPControlLibrary.ConfigHelper.Get<CFAPControlLibrary.DataTypes.ActionSortingOption>(string settingName) Line 25 + 0x35 bytes    C#
CFAPControlLibrary.dll!CFAPControlLibrary.ConfigHelper.Get<CFAPControlLibrary.DataTypes.ActionSortingOption>(string settingName, CFAPControlLibrary.DataTypes.ActionSortingOption defaultVal) Line 15 + 0x9 bytes    C#    CFAPControlLibrary.dll!CFAPControlLibrary.DataTypes.ActionStorage.Sort(System.Collections.Generic.List<CFAPControlLibrary.DataTypes.ActionClass> subject) Line 167 + 0xe bytes    C#
CFAPControlLibrary.dll!CFAPControlLibrary.DataTypes.ActionStorage.GetByStatus(string pStatus) Line 162 + 0x46 bytes    C#
CFAPControlLibrary.dll!CFAPControlLibrary.ActionSelector.FillNodes() Line 48 + 0x26 bytes    C#
CFAPControlLibrary.dll!CFAPControlLibrary.CFAPMain.OnActionDetailsArrived(CFAPControlLibrary.CFAPMain.RawActionDetails bwr) Line 371 + 0x10 bytes    C#
CFAPControlLibrary.dll!CFAPControlLibrary.CFAPMain.OnGetDetailsCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) Line 337 + 0xb bytes    C#
user32.dll!_InternalCallWinProc@20()  + 0x23 bytes    
user32.dll!_UserCallWinProcCheckWow@32()  + 0xb3 bytes    
user32.dll!_DispatchMessageWorker@8()  + 0xe6 bytes    
user32.dll!_DispatchMessageW@4()  + 0xf bytes    
[External Code]    
CFAPHost.exe!CFAPHost.Program.Main(string[] args) Line 50 + 0x1d bytes    C#
[External Code]    
mscoreei.dll!__CorExeMain@0()  + 0x38 bytes    
mscoree.dll!_ShellShim__CorExeMain@0()  + 0x227 bytes    
mscoree.dll!__CorExeMain_Exported@0()  + 0x8 bytes    
kernel32.dll!@BaseThreadInitThunk@12()  + 0x12 bytes    
ntdll.dll!___RtlUserThreadStart@8()  + 0x27 bytes    
ntdll.dll!__RtlUserThreadStart@8()  + 0x1b bytes

以下是来自顶部堆栈帧的源代码: KernelBase.dll 的反汇编: KernelBase.dll 中的帧

然后是我的代码中的最后一个帧,m_SettingCache 是一个字典,它不包含所请求的键: Base.GetSetting

接下来的几个帧: KernelBase.dll 中的帧 KernelBase.dll 中的帧 KernelBase.dll 中的帧

我认为代码非常简单,只是通用的设置读取,默认值。如果出现问题(设置名称未定义或转换不可能),将返回默认值。代码肯定有效。从转储中看到的是从字典中读取的结果从未返回,尽管它应该抛出KeyNotFoundException,但这从未发生过。有什么建议吗?
注意:主线程确实停在转储捕获的状态中:每次我进行转储时,结果都是相同的。
注意2:hang从未发生在此代码路径的第一次执行上,在每种情况下,此完全相同的代码路径都在hang之前执行(从应用程序日志推断出)。
如有需要,我可以提供更多细节。 提前感谢您。
编辑: CFAPControlLibrary.dll是应用程序的主要程序集。它包含Windows窗体及其相应的逻辑。使用WCF与服务器通信。较大的请求在后台线程中使用BackgroundWorker进行。您在调用堆栈中看到的执行路径是由此类BackgroundWorker的完成事件调用的。
我在此处粘贴了所请求的代码片段here

我的AppDomain.CurrentDomain.UnhandledException处理程序在这里

我最初认为不重要但后来证明很重要的堆栈部分(敏感字符串文字已从图像中删除):

Application.Run的证据 这表明调用了Application.Run,我不知道为什么在调用堆栈中没有显示。

更新

在花费三天时间找不到问题原因后,我决定尝试一个变通方法。由于内存转储显示应用程序总是在应抛出KeyNotFound异常的那一点挂起。最直接的解决方法是重构代码以避免可能的异常抛出。该版本通过了测试,从未挂起。 这根本不是一个解决方案,但我们不能再花更多时间在这上面了。所以基本上我祈祷并希望我永远不会再看到这个崩溃。

感谢所有的建议。


你能否发布你认为引起问题的代码? - Kurubaran
@AccessDenied 我认为相关的代码可以在图片中看到。如果您想以文本形式查看它,那么请回复我,我会在某个地方发布最重要的部分。如果您指的是整个项目,恐怕我不能没有非常充分的理由上传它,因为我的公司认为这是商业机密。 - Hari
1
一些额外的信息可能会有所帮助。CFAPControlLibrary是什么,您是否使用任何线程或异步I/O? - H H
@HenkHolterman 我在问题中添加了一个 Pastebin 链接。代码有点混乱,但是这个项目在我继承之前已经被放弃两次了,要重构它将需要巨大的努力。无论如何,我尽力提取相关部分。 - Hari
你所粘贴的内容帮助有限,因为它是本地调用堆栈。对于.NET应用程序,你应该加载SOS并使用!CLRStack来分析其托管调用堆栈。这将为你提供更多关于快照进程(和线程)以及定位根本原因的见解。除非你对WinDbg和SOS非常熟悉,我建议你通过http://support.microsoft.com打开一个支持案例,并与Microsoft支持团队共享转储文件。 - Lex Li
显示剩余2条评论
1个回答

5
user32.dll!_DispatchMessageW@4()  + 0xf bytes    
[External Code]    
CFAPHost.exe!CFAPHost.Program.Main(string[] args) Line 50 + 0x1d bytes    C#

重写此部分堆栈跟踪,存在严重问题。Main()方法应始终调用Application.Run()以启动消息循环。或者应该存在ShowDialog()调用,这两种是消息可以被分派的正常方式。没有任何一种情况发生,尽管仍在调用DispatchMessage() winapi函数。
CLR中还有一种非常晦涩的方式可使消息泵入。当应用程序在[STAThread]上使用lock语句时,例如GUI应用程序的主线程。或者WaitHandle.WaitOne()或Thread.Join(),其他常见的阻止方法。因为阻止STA线程很可能导致死锁,因此CLR进行泵送以避免麻烦。执行此操作的代码将隐藏在[External Code]部分中。
发布的代码中确实有证据,它在非常不合适的地方使用锁定。在UI代码中使用锁定从来不正确。
因此,在应用程序崩溃时看到死锁也很容易解释。
这是代码中的严重结构性问题,您需要修复它。从Main()方法开始,这很早就出了问题。在您的开发机器上进行简单检查,只需查看调用堆栈即可。

奇怪的是,我已经为AppDomain.CurrentDomain.UnhandledException事件注册了处理程序,但它从未被执行过。至少在我看来是这样,也许处理程序本身太复杂而崩溃了?我将在原帖中添加一个pastebin链接,其中包含我的处理程序。我可以编译一个64位调试版本来测试这种情况,你认为看到更多细节会有所帮助吗? - Hari
谢谢你的提示。我会尝试利用从那个问题中学到的知识。但是只能明天,因为今天我不能再访问生产环境了。关于你的锁定建议:我已经修改了代码,并且所有保护该集合的锁都在GUI线程上,因此锁定完全没有意义。我将它们全部删除了,但是在失败的环境中无法测试它们(在发现所有锁都在同一线程上之后,这里没有太大的期望)。即使我无法追踪问题,删除锁也是一个巨大的收获,非常感谢! - Hari
你基于堆栈跟踪的推理是完全正确的。我希望我在一开始就提供了所有重要信息。结果发现Application.Run被调用了,只是在堆栈跟踪中没有显示出来。我编辑了我的答案,在代码窗口中显示了调用。这仍然是旧的转储文件,今天我将进行几个新版本(重构锁)的测试。 - Hari

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接