僵尸真的存在吗?在。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做了某种处理来保护线程不暴露给可能产生这种影响的系统组件,但这纯粹是推测,可能只是基于我以前从未听说过“僵尸线程”这一事实。所以,我希望我能在这里得到一些反馈:

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

更新

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

Object is in a zombie state. < / >

28001 次浏览

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

不朽之蓝指出,在。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() { }块内部发生了其他异常,并且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中很难犯这个错误,我认为这个问题会被上面提到的其他问题所掩盖。好运!< / p >

< >强最初的回答 如果没有编写适当的线程代码,Zombies__可能存在。对于C/ c++和Java等其他语言也是如此。但这并不是不使用。net编写线程代码的理由。< / p >

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

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

__我第一次听说“僵尸”这个词,但根据你的描述,你的同事可能指的是没有释放所有资源就终止的线程。这可能会导致死锁、内存泄漏或其他不良副作用。这显然是不可取的,但是因为这个可能性而挑出。net可能不是一个好主意,因为在其他语言中也有可能。我甚至认为C/ c++比。net更容易搞砸(特别是在没有RAII的C中),但很多关键的应用程序都是用C/ c++编写的,对吧?所以这取决于你的个人情况。如果你想从你的应用程序中提取每一盎司的速度,并且想要尽可能地接近金属,那么. net 可能不是最好的解决方案。如果你的预算很紧张,并且需要与web服务/现有的。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上出现吗?(为什么/为什么不?)
  • 如果适用,我如何在。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被调用才会被释放-事实证明,创建一个泄露句柄的僵尸线程比我想象的还要困难)

  • 如果适用,我如何在。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被调用时关闭(作为file终结器的一部分,它在后台使用SafeFileHandle来包装其文件句柄)

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

另请参阅

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

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

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

是的,它们会发生。它是一个引用,实际上被Windows称为“zombie”: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清除,但对象周围的lock不会。但你为什么要这么做?