I was having a discussion with a teammate about locking in .NET. He's a really bright guy with an extensive background in both lower-level and higher-level programming, but his experience with lower level programming far exceeds mine. Anyway, He argued that .NET locking should be avoided on critical systems expected to be under heavy-load if at all possible in order to avoid the admittedly small possibility of a "zombie thread" crashing a system. I routinely use locking and I didn't know what a "zombie thread" was, so I asked. The impression I got from his explanation is that a zombie thread is a thread that has terminated but somehow still holds onto some resources. An example he gave of how a zombie thread could break a system was a thread begins some procedure after locking on some object, and then is at some point terminated before the lock can be released. This situation has the potential to crash the system, because eventually, attempts to execute that method will result in the threads all waiting for access to an object that will never be returned, because the thread that is using the locked object is dead.

I think I got the gist of this, but if I'm off base, please let me know. The concept made sense to me. I wasn't completely convinced that this was a real scenario that could happen in .NET. I've never previously heard of "zombies", but I do recognize that programmers who have worked in depth at lower levels tend to have a deeper understanding of computing fundamentals (like threading). I definitely do see the value in locking, however, and I have seen many world class programmers leverage locking. I also have limited ability to evaluate this for myself because I know that the lock(obj) statement is really just syntactic sugar for:

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

因为班长。进入并监控。出口被标记为extern。似乎可以想象,. net做了某种处理来保护线程不暴露给可能产生这种影响的系统组件,但这纯粹是推测,可能只是基于我以前从未听说过“僵尸线程”这一事实。所以,我希望我能在这里得到一些反馈:

有没有比我在这里解释的更明确的“僵尸线程”定义呢? 僵尸线程会在。net上出现吗?(为什么/为什么不?) 如果适用,我如何在。net中强制创建僵尸线程? 如果适用,我如何在。net中利用锁定而不冒僵尸线程场景的风险?

更新

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


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

不朽之蓝指出,在。net 2.0及更高版本中,最终块对线程中止免疫。正如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(){}块内部发生了其他异常,并且ThreadAbortException恰好在finally块即将运行时到达,则不会释放锁。正如你提到的,lock(){}块被编译为:

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

如果另一个线程在生成的finally块中调用thread . abort(),锁可能不会被释放。


在高负载的关键系统上,编写无锁代码更好,主要是因为性能改进。看看《LMAX》之类的游戏,看看它是如何利用“机械同情”来进行相关讨论的。担心僵尸帖子吗?我认为这是一个边缘情况,只是一个需要解决的bug,而不是一个不使用lock的足够好的理由。

听起来更像是你的朋友只是在向我炫耀他对晦涩难懂的异国术语的知识!在我在微软英国运行性能实验室的所有时间里,我从未在。net中遇到过这个问题的实例。


我把我的答案整理了一下,但把原来的答案留在下面供参考

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

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

因此,根据这个定义,是的,你可以在。net中这样做,就像其他语言(C/ c++, java)一样。

然而,我不认为这是一个不使用。net编写线程化的关键任务代码的好理由。可能有其他原因决定不使用。net,但仅仅因为你可以有僵尸线程就放弃。net对我来说没有意义。僵尸线程在C/ c++中是可能的(我甚至认为在C中更容易搞砸),许多关键的线程应用程序在C/ c++中(大量交易,数据库等)。

结论 如果你正在决定使用一种语言,那么我建议你从大局考虑:性能、团队技能、进度、与现有应用的集成等。当然,僵尸线程是你应该考虑的事情,但是由于与C等其他语言相比,在。net中很难犯这个错误,我认为这个问题会被上面提到的其他问题所掩盖。好运!

原来的答案 僵尸†可以存在,如果你不写适当的线程代码。对于C/ c++和Java等其他语言也是如此。但这并不是不使用。net编写线程代码的理由。

就像使用其他语言一样,在使用某些东西之前要知道价格。它还有助于了解底层发生了什么,从而可以预见任何潜在的问题。

为关键任务系统编写可靠的代码并不容易,无论您使用何种语言。但我确信在。net中也不是不可能做到。另外,.net线程与C/ c++中的线程并没有什么不同,它使用(或由)相同的系统调用,除了一些.net特定的结构(如RWL和事件类的轻量级版本)。

†first time I've heard of the term zombies but based on your description, your colleague probably meant a thread that terminated without release all resources. This could potentially cause a deadlock, memory leak or some other bad side effect. This is obviously not desirable but singling out .NET because of this possibility is probably not a good idea since it’s possible in other languages too. I'd even argue that it’s easier to mess up in C/C++ than in .NET (especially so in C where you don't have RAII) but a lot of critical apps are written in C/C++ right? So it really depends on your individual circumstances. If you want to extract every ounce of speed from your application and want to get as close to bare metal as possible, then .NET might not be the best solution. If you are on a tight budget and do a lot of interfacing with web services/existing .net libraries/etc then .NET may be a good choice.


这不是关于僵尸线程的,但是《Effective c#》这本书有一个关于实现IDisposable的部分(第17项),其中讨论了僵尸对象,我认为你可能会感兴趣。

我推荐阅读这本书本身,但它的要点是,如果你有一个实现IDisposable的类,或者包含一个析构函数的类,你在其中做的唯一一件事就是释放资源。如果在这里执行其他操作,则该对象有可能不会被垃圾收集,但也不能以任何方式访问。

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

internal class Zombie
{
    private static readonly List<Zombie> _undead = new List<Zombie>();

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

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

下面是一个更完整的示例。当foreach循环到达时,Undead列表中有150个对象,每个对象都包含一个图像,但是图像已经被GC处理了,如果你试图使用它,你会得到一个异常。在这个例子中,当我尝试对图像做任何事情时,我得到了一个ArgumentException (Parameter是无效的),无论是我尝试保存它,甚至是查看高度和宽度等维度:

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中的僵尸的,我想到了这一点,并且认为其他人可能会觉得很有趣!


有没有比我在这里解释的更明确的“僵尸线程”定义呢?

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

僵尸线程会在。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);
    }
}

这个程序启动一个线程目标,它打开一个文件,然后立即使用ExitThread杀死自己。由此产生的僵尸线程将永远不会释放“test.txt”文件的句柄,因此该文件将保持打开状态,直到程序终止(您可以使用进程资源管理器或类似工具进行检查)。“test.txt”的句柄直到GC才会被释放。收集被称为-事实证明,甚至比我想象的更难创建一个僵尸线程泄漏处理)

如果适用,我如何在。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);
}

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

C.Evenhuis中的锁定示例可能是在线程以正常方式终止时无法释放资源(在这种情况下是锁)的最简单方法,但这很容易通过使用lock语句或将释放放在finally块中来修复。

另请参阅

c# IL的微妙之处 codegen 这是一个非常微妙的情况,异常可以阻止锁从 即使使用lock关键字也会被释放(但仅限于。net 3.5及更早版本) 锁和异常则不然 混合


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

我同意“僵尸线程”的存在,这是一个术语,指的是线程留下的资源,它们不放手,但没有完全死亡,因此得名“僵尸线程”,所以你对这个提到的解释是非常正确的!

2.僵尸线程会在。net上出现吗?(为什么/为什么不?)

是的,它们会发生。这是一个参考,实际上在Windows中被称为“僵尸”:MSDN使用“僵尸”这个词来表示已经死亡的进程/线程

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

是的,正如@KevinPanko在评论中正确提到的那样,“僵尸线程”确实来自Unix,这就是为什么它们在xcode - objective - c中被使用,并被称为“NSZombie”并用于调试。它的行为几乎是一样的……唯一的区别是一个应该已经死亡的对象变成了一个用于调试的“僵尸对象”,而不是“僵尸线程”,这可能是你代码中的一个潜在问题。


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

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

这将泄露线程句柄(对于Join())。就我们所关心的托管世界而言,这只是另一种内存泄漏。

现在,以一种实际持有锁的方式杀死线程是一种痛苦,但却是可能的。另一个人的ExitThread()完成这项工作。他发现,文件句柄被gc清除,但对象周围的锁不会。但你为什么要这么做?