长时间运行过程暂停

时间:2013-01-29 06:59:33

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

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

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

我已经对这个问题进行了几个月的描述,但是我已经没想完了。

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

任何人都有任何提示来诊断究竟发生了什么?

3 个答案:

答案 0 :(得分:16)

  

它也是多线程的

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

它可以从信息中进一步缩小,显然你的过程没有完全冻结,因为它仍然消耗100%的cpu。您的代码中可能有一个热等待循环,一个循环在另一个发出事件信号的线程上。这可能导致一种特别令人讨厌的各种僵局,live-lock。实时锁定对时序非常敏感,代码运行顺序的微小变化可能会将其变为实时锁定。再次退出。

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

讨厌的东西,不可能从像SO这样的网站获得这样的问题的帮助,因为它非常依赖于代码。通常需要彻底检查代码才能找到原因。并不是很少重写。祝你好运。

答案 1 :(得分:2)

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

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

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

如果您有上述内容,可以尝试通过使超时和睡眠具有随机间隔来缓解问题,并在可能产生错误的情况下添加短暂的随机持续时间睡眠死区/活锁。如果这样的循环对性能敏感,则添加一个计数器,并且只能在一些失败的重试次数后随机开始休眠,或许会增加间隔。并且确保你添加的任何睡眠都不会在某些东西被锁定时睡觉。

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

答案 2 :(得分:0)

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

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

注意如何以最小的内存泄漏结束,如果你还使用100%CPU进程删除Thread.Sleep - 即使主线程仍在响应。因为它们是不同的线程,所以从这里可以很容易地阻止整个过程 - 例如通过使用锁:

    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";
    }