关于c#:僵尸存在……在.NET中?

Do zombies exist … in .NET?

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

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

1
2
3
4
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中僵尸线程场景的风险的情况下利用锁定?
  • 更新

    两年前我问过这个问题。今天发生了这样的事:

    Object is in a zombie state.


    • "僵尸线"的定义是否比我在这里解释的更清楚?

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

    • .NET上是否可以出现僵尸线程?(为什么/为什么不呢?)
    • 如果适用,如何在.NET中强制创建僵尸线程?

    他们当然有,看,我做了一个!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    [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"文件的句柄,因此该文件将保持打开状态,直到程序终止(您可以使用Process Explorer或类似工具进行检查)。the handle to"test.txt"won't be released until GC.Collectis called-it turns out it is even more difficuble than I thought to create一条僵尸线,泄露把手)

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

    别做我刚做的事!

    只要您的代码在正确地清理完自己之后(如果使用非托管资源,请使用安全句柄或等效类),并且只要您不以奇怪而奇妙的方式杀掉线程(最安全的方法就是永远不要杀掉线程-让它们正常终止,或者在必要时通过异常终止),那么你将得到类似僵尸线程的东西的方式是,如果发生了非常错误的事情(例如,在clr中发生了错误)。

    事实上,创建僵尸线程实际上非常困难(我必须对文档中的函数p/invoke进行调用,该函数本质上告诉您不要在C之外调用它)。例如,下面的(糟糕的)代码实际上不会创建僵尸线程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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的细微之处代码生成对于非常微妙的情况,异常可以阻止即使使用lock关键字(但仅在.NET 3.5及更早版本中)也会被释放。
    • 锁和异常不会混合


    我把我的答案整理了一下,但把原来的留作参考。

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

    在不释放所有资源的情况下终止的线程

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

    但是,我不认为这是不在.NET中编写线程化、关键任务代码的一个好理由。可能还有其他的原因决定反对.NET,但是仅仅因为你可以拥有僵尸线程而注销.NET对我来说是没有意义的。僵尸线程在C/C++中是可能的(我甚至争辩说,C中容易搞乱),很多关键的、线程化的应用程序都在C/C++(大容量交易、数据库等)中。

    结论如果你正在决定使用哪种语言,那么我建议你从全局考虑:性能、团队技能、日程安排、与现有应用程序的集成等。当然,僵尸线程是你应该考虑的,但是与其他语言相比,在.NET中确实很难犯下这个错误。像C一样,我认为这种担心会被其他事情所掩盖,比如上面提到的。祝你好运!

    原始答案僵尸?如果编写的线程代码不正确,则可能存在。对于C/C++和Java等其他语言也是如此。但这并不是不在.NET中编写线程代码的原因。

    就像其他语言一样,在使用之前要知道价格。它还可以帮助您了解引擎盖下发生了什么,这样您就可以预见到任何潜在的问题。

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

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


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

    不朽蓝指出,在.NET 2.0和更高版本中,finally块不受线程中止的影响。正如Andreas Niedermir所评论的,这可能不是一个真正的僵尸线程,但下面的示例说明了中止线程如何导致问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    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() { }块内发生其他异常,并且ThreadAbortException刚好在finally块即将运行时到达,则不会释放锁。正如您所提到的,lock() { }块编译为:

    1
    2
    3
    4
    5
    finally
    {
        if (lockWasTaken)
            Monitor.Exit(temp);
    }

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


    这不是关于僵尸线程的,但是这本书有效的C有一个关于实现IDisposable的部分(第17项),它讨论了僵尸对象,我认为您可能会发现这一点很有趣。

    我建议您阅读这本书本身,但其要点是,如果您有一个实现IDisposable的类,或者包含一个描述器,那么您在这两个类中唯一应该做的就是释放资源。如果您在这里执行其他操作,那么对象可能不会被垃圾收集,但也不会以任何方式被访问。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    internal class Zombie
    {
        private static readonly List<Zombie> _undead = new List<Zombie>();

        ~Zombie()
        {
            _undead.Add(this);
        }
    }

    当调用此对象上的析构函数时,对其本身的引用将被放置在全局列表中,这意味着它在程序的生命周期内保持活动状态和内存中,但不可访问。这可能意味着资源(尤其是非托管资源)可能没有完全释放,这可能导致各种潜在问题。

    下面是一个更完整的例子。当到达foreach循环时,在undead列表中有150个对象,每个对象都包含一个图像,但是该图像已经被gc处理过,如果您尝试使用它,就会得到一个异常。在本例中,当我尝试对图像执行任何操作时,无论是尝试保存图像,还是甚至查看高度和宽度等尺寸,我都会得到一个argumentexception(参数无效):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    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);
        }
    }

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


    在重负载的关键系统上,编写无锁代码主要是因为性能改进。看看像lmax这样的东西,以及它是如何利用"机械同情心"对此进行大量讨论的。但是担心僵尸线程吗?我认为这是一个边缘案例,只是一个需要解决的bug,并没有足够的理由不使用lock

    听起来更像你的朋友只是在幻想和炫耀他所知道的一个模糊的外来术语对我来说!在我运行微软英国性能实验室的所有时间里,我从未在.NET中遇到过这个问题的实例。


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

    我确实同意"僵尸线程"的存在,这是一个术语,指的是那些拥有资源的线程所发生的事情,它们不放手,但也不完全死亡,因此命名为"僵尸",所以您对这种转介的解释在金钱上是非常正确的!

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

    是的,它们可能发生。它是一个引用,实际上被Windows称为"僵尸":msdn将"僵尸"一词用于死进程/线程

    频繁发生这是另一个故事,取决于您的编码技术和实践,至于您喜欢线程锁定,并且已经做了一段时间,我甚至不会担心发生在您身上的场景。

    是的,正如注释中正确提到的@kevinpanko,"僵尸线程"确实来自Unix,这就是为什么它们在Xcode Objective EC中被使用,并被称为"nszombie",用于调试的原因。它的行为几乎是一样的…唯一的区别是,一个本应终止的对象变成了用于调试的"僵尸对象",而不是"僵尸线程",这可能是代码中的一个潜在问题。


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

    1
    2
    3
    4
    5
    6
    7
    var zombies = new List<Thread>();
    while(true)
    {
        var th = new Thread(()=>{});
        th.Start();
        zombies.Add(th);
    }

    这会泄漏螺纹手柄(用于Join())。就我们所关心的管理世界而言,这只是另一个内存泄漏。

    现在,以一种能锁住锁的方式杀死一根线是一种痛苦,但这是可能的。另一个人的ExitThread()负责这项工作。正如他发现的那样,GC清理了文件句柄,但对象周围的EDOCX1[2]不会。但是为什么要这样做?