僵尸是否存在......在.NET中?

我正在和一个队友讨论关于锁定.NET的问题。 他是一个非常聪明的人,在低级和高级编程方面有广泛的背景,但他在低级编程方面的经验远远超过我的。 无论如何,他争辩说,为了避免“僵尸线程”出现系统崩溃的可能性很小,在可能的情况下尽可能在重负载的关键系统上应该避免使用.NET锁定。 我经常使用锁定,我不知道什么是“僵尸线程”,所以我问。 我从他的解释中得到的印象是,僵尸线程是终止的线程,但仍以某种方式持有某些资源。 他给出了一个僵尸线程如何破坏系统的例子,一个线程在锁定某个对象后开始某个过程,然后在释放锁之前终止。 这种情况有可能导致系统崩溃,因为最终尝试执行该方法将导致线程全部等待访问永远不会返回的对象,因为正在使用锁定对象的线程已死亡。

我想我有这个要点,但是如果我离开了基地,请告诉我。 这个概念对我有意义。 我并不完全相信这是在.NET中可能发生的真实场景。 我以前从未听说过“僵尸”,但我确实认识到,在较低级别深入工作的程序员往往对计算基础知识(如线程)有更深入的理解。 然而,我确实看到了锁定的价值,并且我看到许多世界级的程序员利用锁定。 我对自己的评估能力有限,因为我知道lock(obj)语句实际上只是语法糖:

bool lockWasTaken = false;
var temp = obj;
try { Monitor.Enter(temp, ref lockWasTaken); { body } }
finally { if (lockWasTaken) Monitor.Exit(temp); }

并且因为Monitor.EnterMonitor.Exit被标记为extern 。 似乎可以想象,.NET会进行某种处理,以保护线程免受系统组件的影响,但这纯粹是推测性的,可能仅仅是基于我从未听说过“僵尸线程”的事实,之前。 所以,我希望我可以在这里得到一些反馈意见:

  • 有没有比我在这里解释的“僵尸线”更清晰的定义?
  • 僵尸线程是否可以在.NET上发生? (为什么/为什么不?)
  • 如果适用,我如何强制在.NET中创建僵尸线程?
  • 如果适用,我如何在.NET中利用锁定而不冒着僵尸线程的风险?
  • 更新

    两年前我问了这个问题。 今天这发生了:

    对象处于僵尸状态。


  • 有没有比我在这里解释的“僵尸线”更清晰的定义?
  • 对我来说这似乎是一个非常好的解释 - 一个已终止的线程(因此可能不再释放任何资源),但其资源(例如句柄)仍然存在并(可能)导致问题。

  • 僵尸线程是否可以在.NET上发生? (为什么/为什么不?)
  • 如果适用,我如何强制在.NET中创建僵尸线程?
  • 他们确实做到了,看,我做了一个!

    [DllImport("kernel32.dll")]
    private static extern void ExitThread(uint dwExitCode);
    
    static void Main(string[] args)
    {
        new Thread(Target).Start();
        Console.ReadLine();
    }
    
    private static void Target()
    {
        using (var file = File.Open("test.txt", FileMode.OpenOrCreate))
        {
            ExitThread(0);
        }
    }
    

    这个程序启动一个线程Target ,它打开一个文件,然后立即使用ExitThread它自己。 由此产生的僵尸线程将永远不会释放“test.txt”文件的句柄,因此文件将保持打开状态,直到程序终止(可以使用进程资源管理器或类似程序进行检查)。 在调用GC.Collect之前,“test.txt”的句柄不会被释放 - 事实证明,这比创建一个泄漏句柄的僵尸线程更困难)

  • 如果适用,我如何在.NET中利用锁定而不冒着僵尸线程的风险?
  • 不要做我刚做的事!

    只要你的代码正确清理完毕(使用安全句柄或者如果使用非托管资源使用等价类),并且只要你不想以奇怪的方式杀死线程(最安全的方法就是永远不要杀线程-让他们自行终止正常,或通过异常如果需要的话),你将会有类似的东西僵尸线程的唯一途径是,如果事情已经非常错误的(如一些在CLR出错)。

    事实上,创建一个僵尸线程实际上是非常困难的(我不得不将P / Invoke变成一个函数,在文档中实际上告诉你不要在C之外调用它)。 例如下面的(可怕的)代码实际上不会创建一个僵尸线程。

    static void Main(string[] args)
    {
        var thread = new Thread(Target);
        thread.Start();
        // Ugh, never call Abort...
        thread.Abort();
        Console.ReadLine();
    }
    
    private static void Target()
    {
        // Ouch, open file which isn't closed...
        var file = File.Open("test.txt", FileMode.OpenOrCreate);
        while (true)
        {
            Thread.Sleep(1);
        }
        GC.KeepAlive(file);
    }
    

    尽管犯了一些非常可怕的错误,但是一旦Abort被调用,“test.txt”句柄仍然是关闭的(作为file终结器的一部分,在封面下使用SafeFileHandle来包装它的文件句柄)

    C.Evenhuis答案中的锁定示例可能是在线程以非奇怪的方式终止时未能释放资源(在这种情况下为锁)的最简单方法,但这很容易通过使用lock语句来修复,或者将该版本放入一个finally块中。

    也可以看看

  • C#IL codegen的微妙之处在于一个非常微妙的情况,即使使用lock关键字,异常也可以防止释放lock (但仅限于.Net 3.5和更早的版本)
  • 锁和例外不会混合

  • 我已经清理了一下我的答案,但留下了原来的一个以供参考

    这是我第一次听说僵尸这个词,所以我会假定它的定义是:

    终止而不释放所有资源的线程

    所以给定了这个定义,那么是的,你可以在.NET中做到这一点,就像其他语言一样(C / C ++,Java)。

    但是 ,我不认为这是不用.NET编写线程化的任务关键代码的好理由。 可能还有其他的理由来决定对付.NET,只是因为你可以让僵尸线程以某种方式对我没有意义。 在C / C ++中僵尸线程是可能的(我甚至认为在C中搞砸要容易得多),而且很多关键的线程应用程序都是C / C ++(大量交易,数据库等)。

    结论如果您正在决定使用哪种语言,那么我建议您考虑一下整体情况:性能,团队技巧,时间安排,与现有应用程序的集成等。当然,僵尸线程是您应该思考的问题,但是由于与.NET等其他语言相比实际犯这个错误非常困难,我认为这个问题会被上面提到的其他问题所掩盖。 祝你好运!

    原始答案僵尸†可以存在,如果你没有写出适当的线程代码。 其他语言(如C / C ++和Java)也是如此。 但这不是不用.NET编写线程代码的原因。

    就像使用任何其他语言一样,在使用某些东西之前知道价格。 这也有助于了解发动机罩下发生的情况,以便预见任何潜在的问题。

    对于任务关键型系统而言,可靠的代码不容易编写,无论您使用何种语言。但是,我认为在.NET中正确使用并不是不可能的。 此外,AFAIK,.NET线程与C / C ++中的线程没有什么不同,它使用(或由其构建)相同的系统调用,除了一些.net特定的构造(如RWL和事件类的轻量级版本)。

    †第一次我听说过僵尸这个术语,但是根据你的描述,你的同事可能意味着一个终止而不释放所有资源的线程。 这可能会导致死锁,内存泄漏或其他不良副作用。 这显然是不可取的,但是因为这种可能性而挑选出.NET可能不是一个好主意,因为它在其他语言中也是可能的。 我甚至会争辩说,在C / C ++中比在.NET中更容易搞砸(特别是在没有RAII的C中),但很多关键应用程序是用C / C ++编写的吗? 所以这取决于你的个人情况。 如果你想从你的应用程序中提取每盎司的速度,并希望尽可能接近裸机,那么.NET可能不是最好的解决方案。 如果您的预算紧张,并且需要与Web服务/现有.net库/等进行大量连接,那么.NET可能是一个不错的选择。


    现在我的答案大部分已被下面的评论纠正。 我不会删除答案,因为我需要声誉点,因为评论中的信息可能对读者有价值。

    不朽的蓝色指出,在.NET 2.0和finally块不受线程中止影响。 正如Andreas Niedermair评论的那样,这可能不是真正的僵尸线程,但以下示例显示了如何中止线程可能导致问题:

    class Program
    {
        static readonly object _lock = new object();
    
        static void Main(string[] args)
        {
            Thread thread = new Thread(new ThreadStart(Zombie));
            thread.Start();
            Thread.Sleep(500);
            thread.Abort();
    
            Monitor.Enter(_lock);
            Console.WriteLine("Main entered");
            Console.ReadKey();
        }
    
        static void Zombie()
        {
            Monitor.Enter(_lock);
            Console.WriteLine("Zombie entered");
            Thread.Sleep(1000);
            Monitor.Exit(_lock);
            Console.WriteLine("Zombie exited");
        }
    }
    

    然而,当使用lock() { }块时,当以这种方式触发ThreadAbortException时, finally仍然会被执行。

    事实证明,以下信息仅对.NET 1和.NET 1.1有效:

    如果在lock() { }块内发生其他异常,并且ThreadAbortExceptionfinally块即将运行时到达,则不会释放该锁。 正如你所提到的, lock() { }块被编译为:

    finally 
    {
        if (lockWasTaken) 
            Monitor.Exit(temp); 
    }
    

    如果另一个线程在生成的finally块内调用Thread.Abort() ,则该锁可能不会被释放。

    链接地址: http://www.djcxy.com/p/16401.html

    上一篇: Do zombies exist ... in .NET?

    下一篇: What is the JavaScript version of sleep()?