长时间运行的进程已挂起

本文关键字:挂起 进程 运行 长时间 | 更新日期: 2023-09-27 17:56:22

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

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

几个月来我一直在诊断这个问题,并且已经没有想法了。

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

任何人都有任何诊断到底发生了什么的技巧吗?

长时间运行的进程已挂起

它也是多线程的

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

它可以从信息中进一步缩小范围,显然您的进程并未完全冻结,因为它仍然消耗 100% CPU。 您的代码中可能有一个热等待循环,该循环在另一个线程上旋转,发出事件信号。 这可能会导致一种特别令人讨厌的死锁,即活锁。 活锁对时间非常敏感,代码运行顺序的微小变化可能会使其变成活锁。 然后又回来了。

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

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

应用程序是否有"死锁恢复/预防"代码?也就是说,锁定定时,然后再次尝试,也许在睡觉后?

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

请注意,此类循环也可以通过事件循环发生,其中代码仅位于某个事件处理程序中。它不必是您自己的代码中的实际循环。尽管情况可能并非如此,但如果应用程序被冻结,则表示事件循环被阻止。

如果您有上述内容,您可以尝试通过将超时和睡眠设置为随机间隔来缓解问题,以及在错误可能产生死/活锁的情况下添加短的随机持续时间睡眠。如果此类循环对性能敏感,请添加一个计数器,并且仅在重试失败次数后随机开始休眠,可能会增加间隔。并确保你添加的任何睡眠在锁定时都不会休眠。

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

这里有三件事...

首先,开始使用.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";
    }