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中利用锁定而不冒僵尸线程场景的风险?

更新

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


当前回答

这不是关于僵尸线程的,但是《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 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(),锁可能不会被释放。

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

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

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

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

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

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

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

僵尸线程会在。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及更早版本) 锁和异常则不然 混合

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

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