首页 文章

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

提问于
浏览
356

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

我想我已经掌握了这个,但如果我完全相信这是一个可能在.NET中发生的真实场景 . 我之前从未听说过"zombies",但我确实认识到,在较低级别深入工作的程序员往往对计算基础知识(如线程)有更深入的了解 . 我确实看到了锁定的 Value ,然而,我看到许多世界级程序员利用锁定 . 我也有能力为自己评估这个,因为我知道 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会做某种处理来保护线程免受可能产生这种影响的系统组件的影响,但这纯粹是推测性的,可能只是基于我希望我能得到一些反馈的事实 . 这里:

  • "zombie thread"的定义是否比我在这里解释的更明确?

  • 僵尸线程可以在.NET上出现吗? (为什么/为什么不?)

  • 如果适用,我如何强制在.NET中创建僵尸线程?

  • 如果适用,如何在不冒.NET的僵尸线程情况下利用锁定?

更新

两年多前我问过这个问题 . 今天发生这种事:

7 回答

  • 231

    1.有一个比我在这里解释的“僵尸线程”更明确的定义吗?

    我确实同意“Zombie Threads”存在,这是一个术语来指代线程所发生的事情,这些线程留下的资源是他们不放弃但却没有完全消亡,因此名称为“僵尸”,所以你的对这个推荐的解释是非常正确的钱!

    2.可以在.NET上出现僵尸线程吗? (为什么/为什么不?)

    是的,他们可以发生 . 它是一个引用,实际上被Windows称为"zombie":MSDN uses the Word "Zombie" for Dead processes/threads

    经常发生这是另一个故事,并且取决于你的编码技巧和实践,对于那些喜欢线程锁定并已经做了一段时间的人,我甚至不担心会发生在你身上的情况 .

    是的,正如@KevinPanko在评论中正确提到的那样,“Zombie Threads”确实来自Unix,这就是为什么它们被用在XCode-ObjectiveC中并被称为“NSZombie”并用于调试 . 它的行为方式几乎相同......唯一的区别是应该死的对象变成用于调试的“ZombieObject”而不是“Zombie Thread”,这可能是代码中的潜在问题 .

  • 3

    我可以很容易地制作僵尸线程 .

    var zombies = new List<Thread>();
    while(true)
    {
        var th = new Thread(()=>{});
        th.Start();
        zombies.Add(th);
    }
    

    这会泄漏线程句柄(对于 Join() ) . 就管理世界而言,这只是另一次内存泄漏 .

    现在,以实际持有锁的方式杀死一个线程是后方的痛苦但可能 . 另一个人的 ExitThread() 完成了这项工作 . 正如他所发现的那样,文件句柄被gc清理干净但是对象周围没有 lock . 但你为什么要那样做?

  • 0
    • Is there a clearer definition of a "zombie thread" than what I've explained here?

    对我来说似乎是一个非常好的解释 - 一个已经终止的线程(因此不再释放任何资源),但其资源(例如句柄)仍然存在且(可能)导致问题 .

    • Can zombie threads occur on .NET? (Why/Why not?)

    • If applicable, How could I force the creation of a zombie thread in .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”文件的句柄,因此文件将保持打开状态,直到程序终止(您可以使用进程资源管理器或类似工具进行检查) . "test.txt"的句柄在 GC.Collect 被调用之前不会被释放 - 事实证明它比创建泄漏句柄的僵尸线程更困难

    • If applicable, How can I leverage locking without risking a zombie thread scenario in .NET?

    不要做我刚才做的事!

    只要你的代码正确地清理自己(如果使用非托管资源,使用Safe Handles或等效的类),并且只要你不以奇怪和奇妙的方式杀死线程(最安全的方法是永远不会杀死线程 - 让它们通常会自行终止,或者在必要时通过例外终止),你想要拥有类似于僵尸线程的东西的唯一方法是,如果某些东西已经错误(例如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);
    }
    

    尽管犯了一些非常糟糕的错误,"test.txt"的句柄仍然会在 Abort 被调用时关闭(作为 file 的终结器的一部分,它的封面使用SafeFileHandle来包装其文件句柄)

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

    也可以看看

  • 25

    这不是关于Zombie线程,但是有效C#这本书有一个关于实现IDisposable的部分,(第17项),它讨论了我认为你可能感兴趣的Zombie对象 .

    我建议阅读本书,但其中的要点是,如果你有一个类实现IDisposable,或者包含一个Desctructor,你应该做的唯一事情就是释放资源 . 如果你在这里做其他事情,那么这个对象有可能不会被垃圾收集,但也无法以任何方式访问 .

    它给出了一个类似于下面的例子:

    internal class Zombie
    {
        private static readonly List<Zombie> _undead = new List<Zombie>();
    
        ~Zombie()
        {
            _undead.Add(this);
        }
    }
    

    当调用此对象上的析构函数时,对自身的引用将放在全局列表中,这意味着它在程序的生命周期中保持活动并在内存中,但是不可访问 . 这可能意味着资源(特别是非托管资源)可能无法完全释放,这可能会导致各种潜在问题 .

    下面是一个更完整的例子 . 到达foreach循环时,Undead列表中有150个对象,每个对象都包含一个图像,但图像已经过GC'd,如果您尝试使用它,则会出现异常 . 在这个例子中,当我尝试对图像做任何事情时,我得到一个ArgumentException(参数无效),我是否尝试保存它,甚至查看高度和宽度等尺寸:

    class Program
    {
        static void Main(string[] args)
        {
            for (var i = 0; i < 150; i++)
            {
                CreateImage();
            }
    
            GC.Collect();
    
            //Something to do while the GC runs
            FindPrimeNumber(1000000);
    
            foreach (var zombie in Zombie.Undead)
            {
                //object is still accessable, image isn't
                zombie.Image.Save(@"C:\temp\x.png");
            }
    
            Console.ReadLine();
        }
    
        //Borrowed from here
        //http://stackoverflow.com/a/13001749/969613
        public static long FindPrimeNumber(int n)
        {
            int count = 0;
            long a = 2;
            while (count < n)
            {
                long b = 2;
                int prime = 1;// to check if found a prime
                while (b * b <= a)
                {
                    if (a % b == 0)
                    {
                        prime = 0;
                        break;
                    }
                    b++;
                }
                if (prime > 0)
                    count++;
                a++;
            }
            return (--a);
        }
    
        private static void CreateImage()
        {
            var zombie = new Zombie(new Bitmap(@"C:\temp\a.png"));
            zombie.Image.Save(@"C:\temp\b.png");
        }
    }
    
    internal class Zombie
    {
        public static readonly List<Zombie> Undead = new List<Zombie>();
    
        public Zombie(Image image)
        {
            Image = image;
        }
    
        public Image Image { get; private set; }
    
        ~Zombie()
        {
            Undead.Add(this);
        }
    }
    

    再一次,我知道你特别询问僵尸线程,但问题 Headers 是关于.net中的僵尸,我被提醒了这一点,并认为其他人可能会觉得它很有趣!

  • 45

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

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

    A thread that has terminated without releasing all of its resources

    因此,给定该定义,然后是,您可以在.NET中执行此操作,就像使用其他语言(C / C,java)一样 .

    However ,我认为这不是在.NET中编写线程,任务关键代码的好理由 . 可能还有其他原因可以决定反对.NET,但只是因为你可以让zombie线程以某种方式解决.NET,甚至认为它更容易陷入C)并且许多关键的线程应用都在C中/ C(大批量交易,数据库等) .

    Conclusion 如果您正在决定使用哪种语言,那么我建议您考虑大局:性能,团队技能,日程安排,与现有应用程序的集成等 . 当然,僵尸线程是您应该考虑的事情但是,由于与C等其他语言相比,在.NET中实际犯这个错误实在是太难了,我认为这种担忧会被上面提到的其他事情所掩盖 . 祝好运!

    Original Answer Zombies†如果您没有编写正确的线程代码,则可以存在 . 其他语言(如C / C和Java)也是如此 . 但这不是不在.NET中编写线程代码的理由 .

    和任何其他语言一样,在使用之前知道价格 . 它还有助于了解引擎盖下发生的情况,以便您可以预见任何潜在的问题 .

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

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

  • 20

    现在,我的大多数答案都已通过以下评论得到纠正 . 我不会删除答案,因为我需要声誉点,因为评论中的信息可能对读者有 Value .

    Immortal Blue指出,在.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() { } 块时, finally 仍将在 ThreadAbortException 被触发时执行 .

    事实证明,以下信息仅适用于.NET 1和.NET 1.1:

    如果在 lock() { } 块内部发生另一个异常,并且 ThreadAbortException 恰好在即将运行 finally 块时到达,则不会释放锁定 . 正如您所提到的, lock() { } 块编译为:

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

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

  • 22

    在重负载的关键系统上,编写无锁代码更好主要是因为性能提升 . 看看像LMAX这样的东西,以及它如何利用"mechanical sympathy"对此进行讨论 . 担心僵尸线程呢?我认为_118206只是一个需要解决的错误,而且不足以让我不使用 lock .

    听起来更像是你的朋友只是喜欢和炫耀他对我的晦涩难懂的外语术语!在我在Microsoft UK运行性能实验室的所有时间里,我从未在.NET中遇到过这个问题的实例 .

相关问题