执行在可能的死锁中停止

本文关键字:死锁 执行 | 更新日期: 2023-09-27 18:29:27

经过一个月的休息,我昨天继续编写我的程序。我没有更改代码上的任何内容,但现在我的应用程序不再启动。在某一点上,它只是中断了执行,似乎陷入了死锁,尽管我不确定它是否真的是死锁,因为它发生在方法返回时——在通常不应该发生的时候。

我不能给你看代码,因为它很大。但我可以肯定地说,除了它自己的线程之外,唯一的操作就是访问一些由Dispatcher调用的UI元素。直到昨天一切都很好,我没有改变任何事情。

这就是它发生的地方:

    internal override Task InitializeAddIns()
    {
        try
        {
            Action action = () => this._addinProvider.InitializeAddins();
            Task t = Task.Factory.StartNew(action);
            return t;
        }
        catch (Exception ex)
        {
            Debugger.Break();
            return null;
        }
    }

呼叫代码:

// Initialize AddIns
splash.SplashText = "SplashScreen:step_searchAddIns".Translate();
this._addinSystem.InitializeAddIns();
splash.SplashText = "SplashScreen:step_startAddIns".Translate();
await Task.Run(() => this._addinSystem.RunAddins());
// Resolve libraries with NativeCompressor
splash.SplashText = "SplashScreen:step_resolveDependencies".Translate();

任务启动并返回"t"。InitializeAddins()-方法成功运行到结束(用调试器检查了它-日志也显示它完全完成)。下一步是标记"action"的声明行(完成时)。然后调试结束,不再发生任何事情。甚至这个Dispatcher钩子都没有被调用:

Dispatcher.CurrentDispatcher.Hooks.DispatcherInactive += (sender, args) => this.Update();

我唯一的假设是某个地方出现了僵局。我无法解释为什么整个执行过程会停止并陷入困境。我就是找不到从哪里开始搜寻的线索。我重新编写了新引入的代码,并添加了一些扩展的锁定方法,这些方法也可以检测死锁。到目前为止未检测到死锁。

由于我不知道是什么原因导致了这个问题,我尝试使用WinDbg和SOSEX来查找错误源。遗憾的是,我没有让WinDbg运行。它确实检查了Symbol服务器,最后的输出如下:

CLRDLL:无法通过mscorwks搜索找到mscordacwks_AMD64_x86_4.0.30319.34209.dllCLRDLL:在路径上找不到"SOS_AMD64_x86_3.0.30319.34209.dll"无法自动加载SOSCLRDLL:已加载DLL mscordacwks_AMD64_x86_4.0.30319.34209.DLLCLR DLL状态:已加载DLL mscordacwks_AMD64_x86_4.0.30319.34209.DLL

虽然它显然加载了一些东西,但我在调用SOSEX时收到了这条消息!dlk命令:

0:028>!dlk无法初始化.NET数据接口。需要mscordacwks.dll的4.0.30319.34209版本。找到并加载正确版本的mscordacwks.dll。有关.cordll命令,请参阅文档。正在检查CriticalSections。。。未检测到死锁。

所以我真的不知道如何进一步修复这个错误。这种行为的原因可能是什么?我甚至没有例外。我已经启用了CLR异常,但甚至没有抛出这些异常。这很奇怪,我通常认为这种封锁确实发生在中间的某个地方,而不是在方法退出之后。。。

执行在可能的死锁中停止

第一步是尝试同步运行代码,而不涉及任何任务。

第二步是检查您是否正确等待。例如,您在呼叫this._addinSystem.InitializeAddIns()时缺少一个等待。这意味着在调用RunAddIns之前,对InitializeAddIns的调用可能不会完成。您应该添加以下内容:

await this._addinSystem.InitializeAddIns();

最后,您可能没有正确等待调用代码。例如,如果您正在等待void返回函数,则调用可能会死锁:

// This may deadlock because you are awaiting a void returning function!
await MyMethod();
//...
public void MyMethod()
{
    await this._addinSystem.InitializeAddIns();
    await Task.Run(() => this._addinSystem.RunAddins());    
}

我找到了这个问题的根源。这是我的Splashscreen,一个简单的窗口,通过这些方法可以访问它,以便更新当前状态(加载插件等等)。这是绝对不线程安全(我想知道为什么它以前工作…)。

我在所有属性中将其更改为以下代码。如果有人能检查一下代码,确认它没有被黑客入侵或是一种糟糕的方法,那就太好了,因为它看起来确实有点棘手。。。但到目前为止,它是有效的。

public string SplashText
{
    get
    {
        using (ThreadLock.Lock(_lock))
        {
            return _splashText;
        }
    }
    set
    {
        if (_dispatcher.CheckAccess())
        {
            _splashText = value;
            OnPropertyChanged();
            return;
        }
        _dispatcher.Invoke(() =>
        {
            _splashText = value;
            OnPropertyChanged();
        });
    }
}

死锁前提条件(为什么您以前没有看到死锁)

要想出现僵局,必须满足4个先决条件。如果其中一个丢失,就不会出现死锁。这些先决条件是:

  • 相互排斥
  • 无优先购买权
  • 等待
  • 循环等待

最后一个也可以命名为"定时"。由于这取决于Windows如何分配CPU时间,因此您可能会在数年内没有死锁。在多核CPU上更可能发生这种情况,因为如果两个线程真的并行执行,那么循环等待更容易实现。

您的符号(为什么无法加载SOSEX)

mscordacwks_AMD64_x86_4.0.30319.34209.dll是一个不存在的文件。请承认,你已经将另一个文件重命名为该文件名,因为你已经看到WinDbg在寻找它。

该名称表示您正尝试使用64位调试器调试32位应用程序。Microsoft不支持此功能。您只能在64位WinDbg中调试64位.NET应用程序,在32位WinDb(也在64位操作系统BTW上运行)中调试32位.NET应用软件。

如果你只有一个64位的转储文件,并且不能重现这个问题,那你就倒霉了。没有办法(我研究了好几次)让它工作,也没有办法将转储从64位转换为32位。

解决问题

除此之外,您使用SOSEX‘!dlk的方法也不错。它应该检测由C#lock语句引起的死锁。

我不同意像Jakob Christensen的回答中所建议的那样使代码同步运行。虽然在小型应用程序中可以做到这一点,但在大型应用程序中这将需要太多的重写。

切换到同步执行再切换回异步执行可能会导致再次出现未被检测到的情况(时间可能已经改变,导致死锁的可能性降低)。

IMHO最好真正理解死锁(这需要对Windows内部结构有一些了解,所以你可能想读这本书)。一旦了解了Windows线程,就可以更好地了解asyncawait

然后我同意Peter Duniho的观点,他说:

如果您只访问GUI线程上的_splashText字段,即在该线程中WPF直接调用的代码中,或者在您已显式调度到该线程的代码中——那么是的。。。您不需要任何其他锁定,因为该字段的所有访问都将在单个线程中同步进行。

请注意,不仅仅存在"GUI线程"。我看到越来越多的开发人员创建了几个UI线程,即有自己消息队列的线程。我希望你只有一个。