【问题标题】:Long running process suspended长时间运行的进程暂停
【发布时间】:2013-01-29 06:59:33
【问题描述】:

我有一个 .NET 2.0 控制台应用程序,在 Visual Studio 2010 IDE 的 Windows Server GoDaddy VPS 上以调试模式 (F5) 运行。

应用程序会定期冻结(就像垃圾收集器暂时暂停执行一样),但在极少数情况下它永远不会恢复执行!

我已经对此进行了几个月的诊断,但我的想法已经不多了。

  • 应用程序以正常的优先级运行(它使用 100% 的 CPU 使用率)。它也是多线程的。
  • 当应用程序冻结时,我可以使用 VS2010 IDE 通过暂停/取消暂停进程来解冻它(因为它正在调试器中运行)。
  • 上次执行的位置,当我暂停冻结的进程时,似乎无关紧要。
  • 冻结时,CPU 使用率仍为 100%。
  • 解冻后,它运行良好,直到下一次冻结。
  • 服务器可能会在两次冻结之间运行 70 天,也可能只运行 24 小时。
  • 内存使用保持相对恒定;没有任何内存泄漏的证据。

任何人有任何提示来诊断到底发生了什么?

【问题讨论】:

  • 没有真正的帮助,但它让我感到困惑:你为什么要在 VS 的调试器中运行应用程序 70 天?将它作为发布版本运行有什么问题,这会给您带来更高的性能、更少的开销,并且可能会让您摆脱由调试器引起的问题,就像您目前所遇到的那样?
  • @Krumelur 我通常每周都会为代码提交补丁,但有一段时间我几个月没有提交任何代码。我不想在没有附加调试器的情况下运行应用程序,除非有理由相信调试器可能是罪魁祸首。
  • 好吧,不使用调试器运行它很容易发现它是否仍然会失败。
  • @Krumelur 等待下一次冻结可能需要 70 天,这听起来不是一个简单的找出方法。同时,没有附加调试器来诊断可能发生的任何其他问题。
  • 这样抽象的问题描述很难说。我建议如下: 1. 在没有调试器的情况下运行您的程序。您可以随时将调试器附加到正在运行的程序以解决您的问题。 2. 进程冻结时转储它。使用 Visual Studio 或 WinDBG + sos 扩展调查转储。或者将转储放在某个地方并在此处发布指向它的链接。还需要随构建生成的 PDB 文件。

标签: c# .net windows multithreading garbage-collection


【解决方案1】:

也是多线程的

这是问题的关键部分。您正在描述一种非常典型的多线程程序可能行为不端的方式。它遇到了死锁,这是线程的典型问题之一。

可以从信息进一步缩小范围,显然您的进程并未完全冻结,因为它仍然消耗 100% 的 cpu。您的代码中可能有一个热等待循环,一个在另一个线程上旋转的循环发出一个事件信号。这可能会导致一种特别讨厌的死锁,live-lock。活锁对时间非常敏感,代码运行顺序的微小变化可能会将其撞到活锁。然后再次退出。

活锁非常难以调试,因为尝试这样做会使条件消失。就像附加调试器或破坏代码一样,足以改变线程时序并将其排除在条件之外。或者在代码中添加日志语句,这是调试线程问题的常用策略。由于日志记录开销而改变了时间,这反过来又可以使活锁完全消失。

讨厌的东西,不可能从像 SO 这样的网站获得解决此类问题的帮助,因为它非常依赖于代码。通常需要对代码进行彻底的审查才能找到原因。并且经常进行剧烈的重写。祝你好运。

【讨论】:

  • 死锁似乎极可能,但我使用的唯一同步工具是lock 语句(并且从不嵌套锁)。主线程在访问它之前将lock来自工作线程的资源,并且工作线程在访问它之前将锁定(并且仅锁定)它自己的资源。没有跨工作线程锁;主线程是访问工作线程的唯一线程,并且一次只能访问 1 个。假设这是真的,调试器将如何解冻这两个线程之间的死锁?时间无关紧要,其他线程也无关紧要。
  • Hmya,我帮不了你。我需要查看代码,但我没有什么可看的。但很明显,您谈论它的方式表明您的方法存在根本缺陷。您从不“锁定资源”。 lock 语句中使用的引用应该始终是 object 类型的专用简单引用,其唯一工作是跟踪代码的状态。它不应该是导致死锁的“资源”。不能锁数据,只能屏蔽代码。
  • @Mr. Smith:调试器如何解冻这两个线程之间的死锁? -> 只是一个例子:调试器在一个进程中停止,给另一个进程时间来完成。一进门按F5,其他进程早就结束了,死锁消失了,一切又恢复了流畅。
  • best practicelockobject 的私有实例,但不这样做肯定不是根本缺陷;如果您不遵循最佳实践,那么您需要非常了解lock(this)lock(typeof(MyType)) 和(唯一一个曾经愚弄过我的)lock(some_instance_of_a_string) 的危险。
  • 做对了不需要任何成本。但这不是很相关,从死锁的角度考虑这个问题是没有成效的。死锁的程序不会烧掉 100% 的内核。强烈的迹象是活锁,专注于尝试找到解释为什么您的程序会满负荷运行但没有任何进展,您将有更好的诊断问题的机会。
【解决方案2】:

应用程序是否有“死锁恢复/预防”代码?也就是说,用 timout 锁定,然后再试一次,也许是在睡眠之后?

应用程序是否检查错误代码(返回值或异常)并在任何地方出现错误时反复重试?

请注意,这种循环也可以通过事件循环发生,您的代码仅在某些事件处理程序中。它不必是您自己的代码中的实际循环。虽然情况可能并非如此,但如果应用程序被冻结,则表明事件循环被阻塞。

如果您有上述情况,您可以尝试缓解问题,方法是使超时和休眠具有随机间隔,以及在可能产生错误的情况下添加短随机持续时间的休眠死/活锁。如果这样的循环对性能敏感,请添加一个计数器并仅开始随机休眠,可能会在一些失败的重试次数后增加间隔。并确保您添加的任何睡眠都不会在某些东西被锁定时进入睡眠状态。

如果这种情况会更频繁地发生,您还可以使用它来平分您的代码并查明哪些循环(因为 100% 的 CPU 使用率意味着一些非常繁忙的循环正在旋转)负责。但从问题的罕见性来看,如果问题在实践中消失,我猜你会很高兴;)

【讨论】:

  • 没有死锁恢复/预防代码。有几个地方我catch{} 但我不重试;我catch{} 因为这些地方可能由于普通(并且很好理解)原因(例如套接字关闭)而失败。在我catch{} 不尝试重新发送的情况下,我只是优雅地失败了。如果问题消失了,我会很高兴,但我也会接受解释原因的答案(即使没有解决方案)。
【解决方案3】:

这里有三件事......

首先,开始使用.NET的服务器GC:http://msdn.microsoft.com/en-us/library/ms229357.aspx。这可能会使您的应用程序不被阻塞。

其次,如果您可以在 VM 上执行此操作:检查更新。这似乎总是很明显,但我见过很多情况下简单的 Windows 更新可以解决奇怪的问题。

第三,我想说明对象的生命周期,这可能是这里的问题之一。这是一个很长的故事,所以请耐心等待。

对象的生命周期基本上是构造-垃圾收集-终结。所有三个进程都在一个单独的线程中运行。 GC 将数据传递给终结线程,该线程有一个调用“析构函数”的队列。

如果你的终结器做了一些奇怪的事情怎么办,比如:

public class FinalizerObject
{
    public FinalizerObject(int n)
    {
        Console.WriteLine("Constructed {0}", n);
        this.n = n;
    }

    private int n;

    ~FinalizerObject()
    {
        while (true) { Console.WriteLine("Finalizing {0}...", n); System.Threading.Thread.Sleep(1000); }
    }
}

因为终结器在处理队列的单独线程中运行,所以拥有一个执行愚蠢操作的终结器对您的应用程序来说是一个严重的问题。您可以通过使用上述类 2 次来看到这一点:

    static void Main(string[] args)
    {
        SomeMethod();
        GC.Collect(GC.MaxGeneration);
        GC.WaitForFullGCComplete();
        Console.WriteLine("All done.");
        Console.ReadLine();
    }

    static void SomeMethod()
    {
        var obj2 = new FinalizerObject(1);
        var obj3 = new FinalizerObject(2);
    }

请注意,如果您删除 Thread.Sleep 并同时使用 100% 的 CPU 进程 - 即使您的主线程仍在响应。因为它们是不同的线程,所以从这里开始很容易阻塞整个进程——例如使用锁:

    static void Main(string[] args)
    {
        SomeMethod();
        GC.Collect(GC.MaxGeneration);
        GC.WaitForFullGCComplete();
        Thread.Sleep(1000);
        lock (lockObject)
        {
            Console.WriteLine("All done.");
        }
        Console.ReadLine();
    }

    static object lockObject = new Program();

    static void SomeMethod()
    {
        var obj2 = new FinalizerObject(1, lockObject);
        var obj3 = new FinalizerObject(2, lockObject);
    }

    [...]

    ~FinalizerObject()
    {
        lock (lockObject) { while (true) { Console.WriteLine("Finalizing {0}...", n); System.Threading.Thread.Sleep(1000); } }
    }

所以我可以看到你在想“你是认真的吗?”;事实是,您可能正在做这样的事情,甚至没有意识到这一点。这就是“收益”出现的地方:

'yield' 中的 IEnumerable 实际上是 IDisposable,因此实现了 IDisposable 模式。将你的“yield”实现与锁结合起来,忘记通过用“MoveNext”等枚举它来调用 IDisposable,你会得到一些反映上述情况的非常讨厌的行为。特别是因为终结器是由一个单独的线程(!)从终结队列中调用的。将它与无限循环或线程不安全代码结合起来,你会得到一些非常讨厌的意外行为,这些行为会在特殊情况下触发(当内存耗尽时,或者当 GC 事情它应该做的事情时)。

换句话说:我会检查您的一次性用品和终结器,并对它们非常挑剔。检查 'yield' 是否有隐式终结器,并确保从同一个线程调用 IDisposable。一些你要警惕的事情的例子:

    try
    {
        for (int i = 0; i < 10; ++i)
        {
            yield return "foo";
        }
    }
    finally
    {
        // Called by IDisposable
    }

    lock (myLock) // 'lock' and 'using' also trigger IDisposable
    {
        yield return "foo";
    }

【讨论】:

  • 顺便说一句:我见过一个真实的案例,您描述的“调试器”解冻实际上是问题 (3) 与非托管代码(在我的案例中为 Socket IO)相结合。 Thread.Abort 在这些情况下不起作用,但是您的调试器有时会在这里做一些奇怪的事情。不幸的是,我不能给你一个最低限度的测试用例;很难复制。
  • 我不知道我的环境默认使用哪个 GC(但只是因为我并不真正关心)。话虽如此,MSDN 建议不要在单核机器上使用服务器 GC。此外,它指出如果服务器 GC 与 .NET 4.0 或更早版本一起使用,则并发 GC(这将减少阻塞所有线程的需要)不可用。我倾向于每隔一周在机器上运行一次 Windows 更新,所以它不会落后于更新。最后,我的代码不包含终结器。
  • @Mr.Smith 嗯,这让事情变得复杂了。老实说,如果您没有任何隐式终结器,我会感到惊讶,它们几乎在 .NET 框架中的任何地方都使用过……这说明您的反应肯定会使事情复杂化。我会尝试使用一个名为“托管堆栈资源管理器”的工具,并在它冻结时转储所有线程的所有堆栈跟踪,以便更深入地了解究竟发生了什么。您也可以尝试在 Mono 而不是 MS .NET 中运行它,看看它是否是运行时错误。
  • 托管堆栈资源管理器看起来非常方便;我已经升级到 VS2012 Express(它确实有一个适当的多线程调试器),所以在下一次冻结时,我应该能够查看所有线程的状态。至于使用 Mono 的建议,这个应用程序需要 Microsoft SQL Server。虽然我编写的 .NET Socket 代码已经在 Mono 框架上运行多年(在项目的其他部分)并且很稳定。
猜你喜欢
  • 2010-11-12
  • 1970-01-01
  • 2014-02-11
  • 2018-06-03
  • 2019-02-18
  • 2023-04-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多