僵尸是否存在…在.NET中?
我正和一个队友讨论关于locking.NET的问题。 他是一个非常聪明的人,在低级和高级程序devise方面有着广泛的背景,但是他在低级程序devise方面的经验远远超过了我。 无论如何,他争辩说,为了避免“僵尸线程”崩溃系统的可能性很小,在可能的情况下尽可能在重负载的关键系统上应该避免使用.NETlocking。 我经常使用locking,我不知道什么是“僵尸线程”,所以我问。 我从他的解释中得到的印象是,僵尸线程是一个已经终止的线程,但是仍然保留了一些资源。 他给出了一个僵尸线程如何破坏系统的例子,线程在locking某个对象之后开始某个过程,然后在释放锁之前终止。 这种情况有可能导致系统崩溃,因为最终尝试执行该方法将导致线程全部等待访问永远不会返回的对象,因为使用locking对象的线程已经死亡。
我想我有这个要点,但是如果我离开了基地,请让我知道。 这个概念对我有意义。 我并不完全相信这是在.NET中可能发生的真实场景。 我以前从来没有听说过“僵尸”,但是我确实认识到,在较低层次深入工作的程序员往往对计算基础(比如线程)有更深的理解。 我确实看到locking的价值,然而,我看到许多世界级的程序员利用locking。 我也有限的能力来评估自己,因为我知道lock(obj)
语句实际上只是语法糖:
bool lockWasTaken = false; var temp = obj; try { Monitor.Enter(temp, ref lockWasTaken); { body } } finally { if (lockWasTaken) Monitor.Exit(temp); }
并且因为Monitor.Enter
和Monitor.Exit
被标记为extern
。 似乎可以想象的是,.NET会做一些处理来保护线程免受系统组件的影响,但这纯粹是一种猜测,可能仅仅是因为我从来没有听说过“僵尸线程”之前。 所以,我希望我能在这里得到一些反馈:
- 有没有比我在这里解释的“僵尸线”更清晰的定义?
- 僵尸线程可以发生在.NET上吗? (为什么?为什么不?)
- 如果适用,我如何强制在.NET中创build一个僵尸线程?
- 如果适用,如何在.NET中利用locking而不冒着僵尸线程的风险?
更新
两年前我问了一下这个问题。 今天这发生了:
- 有没有比我在这里解释的“僵尸线”更清晰的定义?
对我来说,这似乎是一个很好的解释 – 一个已经终止的线程(因此可能不再释放任何资源),但是其资源(例如句柄)仍然在(可能)导致问题。
- 僵尸线程可以发生在.NET上吗? (为什么?为什么不?)
- 如果适用,我如何强制在.NET中创build一个僵尸线程?
他们确实做,看,我做了一个!
[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”文件的句柄,因此文件将保持打开状态,直到程序终止(可以使用进程资源pipe理器或类似的方式进行检查)。 在调用GC.Collect
之前,“test.txt”的句柄不会被释放 – 事实certificate,这比创build一个泄漏句柄的僵尸线程更困难)
- 如果适用,如何在.NET中利用locking而不冒着僵尸线程的风险?
不要做我刚做的事!
只要你的代码正确清理完毕(使用安全句柄或使用非托pipe资源的等价类),并且只要你不想以奇怪的方式杀死线程(最安全的方法就是永远不要杀死线程 – 让它们正常地终止自己,或者在必要时通过exception终止),那么你将有类似于僵尸线程的唯一方式是,如果某些事情已经非常错误(例如CLR中出现问题)。
事实上,创build一个僵尸线程(我必须将P / Invoke变成一个函数,实际上告诉你在文档中不要将它叫做C之外)实际上是非常困难的。 例如下面的(可怕的)代码实际上不会创build一个僵尸线程。
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); }
尽pipe犯了一些非常糟糕的错误,“test.txt”的句柄在Abort
被调用时仍然是closures的(作为file
的终结器的一部分,在封面下使用SafeFileHandle来包装它的文件句柄)
C.Evenhuis答案中的locking示例可能是在线程以非奇怪的方式终止时无法释放资源(在这种情况下为锁)的最简单方法,但是可以通过使用lock
语句来轻松修复,或者把这个版本放在一个finally
块中。
也可以看看
- C#IL codegen的微妙之处在于一个非常微妙的情况,即使使用
lock
关键字,exception也可以防止释放lock
(但仅限于.net 3.5及更早的版本) - 锁和例外不混合
我已经清理了一下我的答案,但留下了原来的一个以供参考
这是我第一次听说“僵尸”这个词,所以我认为它的定义是:
终止而不释放所有资源的线程
所以给定这个定义,那么是的,你可以在.NET中做到这一点,就像其他语言一样(C / C ++,Java)。
但是 ,我不认为这是一个不使用.NET编写线程化,关键任务代码的好理由。 也许有其他的理由来决定对付.NET,只是因为你可以有僵尸线程不知道对我来说没有意义。 僵尸线程在C / C ++中是可能的(我甚至认为在C中搞砸要容易得多),而且很多关键的线程应用程序都是C / C ++(大批量交易,数据库等)。
结论如果您正在决定使用哪种语言,那么我build议您考虑一下整体情况:性能,团队技巧,时间表,与现有应用程序的集成等等。当然,僵尸线程是您应该思考的问题,但是由于在.NET中与其他语言(如C)相比实际上犯这个错误是非常困难的,我认为这个问题会被上面提到的其他问题所掩盖。 祝你好运!
原始答案僵尸†可以存在,如果你不写正确的线程代码。 C / C ++和Java等其他语言也是如此。 但这不是不用.NET编写线程代码的原因。
就像使用其他语言一样,在使用某些东西之前知道价格。 这也有助于了解发生了什么事情,所以你可以预见任何潜在的问题。
关键任务系统的可靠代码并不容易编写,无论你使用什么语言。但是我认为在.NET中正确使用它并不是不可能的。 另外,AFAIK,.NET线程与C / C ++中的线程没有什么不同,除了一些.net特定的构造(比如RWL和事件类的轻量级版本)之外,它使用(或者构build)相同的系统调用。
† 第一次我听说过僵尸一词,但根据你的描述,你的同事可能是指一个终止而不释放所有资源的线程。 这可能会导致死锁,内存泄漏或其他不良的副作用。 这显然是不可取的,但是因为这种可能性而单独出现.NET可能不是一个好主意,因为在其他语言中也是可能的。 我甚至会争辩说,在C / C ++中比在.NET中更容易搞砸(特别是在没有RAII的C中),但是很多关键的应用程序是用C / C ++编写的吗? 所以这真的取决于你的个人情况。 如果你想从你的应用程序中提取每盎司的速度,并希望尽可能接近裸机,那么.NET 可能不是最好的解决scheme。 如果你的预算紧张,并与Web服务/现有的.net库等接口很多,那么.NET 可能是一个不错的select。
现在我的答案大部分已经被下面的评论纠正了。 我不会删除答案, 因为我需要声誉点,因为评论中的信息可能对读者有价值。
不朽的蓝色指出,在.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
仍然会被执行。
事实certificate,以下信息仅对.NET 1和.NET 1.1有效:
如果在lock() { }
块内发生其他exception,并且ThreadAbortException
在finally
块即将运行时到达,则不会释放该锁。 正如你所提到的, lock() { }
块被编译为:
finally { if (lockWasTaken) Monitor.Exit(temp); }
如果另一个线程在生成的finally
块内调用Thread.Abort()
,则locking可能不会被释放。
这不是关于僵尸线程,但有效的C#有一个关于实现IDisposable的部分(项目17),其中谈到僵尸对象,我认为你可能会觉得有趣。
我推荐阅读本书,但其要点是,如果你有一个实现IDisposable的类,或者包含一个Desculctor,那么你应该做的唯一一件事是释放资源。 如果你在这里做其他事情,那么这个物体就不会被垃圾收集,但是也不会以任何方式被访问。
它给出了一个类似于下面的例子:
internal class Zombie { private static readonly List<Zombie> _undead = new List<Zombie>(); ~Zombie() { _undead.Add(this); } }
当这个对象的析构函数被调用时,对自身的引用被放置在全局列表中,这意味着它在程序的整个生命周期中保持活动状态,但是不可访问。 这可能意味着资源(特别是非托pipe资源)可能没有完全释放,这可能会导致各种潜在的问题。
下面是一个更完整的例子。 到达foreach循环时,Undead列表中有150个对象,每个对象都包含一个图像,但图像已经GC'd,如果您尝试使用它,则会得到一个exception。 在这个例子中,当我尝试对图像进行任何操作时,无论是否尝试保存图像,甚至查看尺寸(如高度和宽度),都会收到一个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); } }
再一次,我知道你特别是在问僵尸线程,但问题标题是关于.net中的僵尸,我想起了这一点,并认为其他人可能会觉得它很有趣!
在重负载的关键系统上,写无锁代码主要是因为性能的提高。 看看像LMAX这样的东西,以及如何利用“机械同情”来讨论这个问题。 担心僵尸线程呢? 我认为这是一个边缘案例,这只是一个需要解决的问题,而不是一个足够的理由不使用lock
。
听起来更像是你的朋友只是幻想和炫耀他对我的陌生的异国名词术语! 在我一直在Microsoft UK运行性能实验室的时候,我从未在.NET中遇到这个问题的一个实例。
1.“僵尸线”的定义是否比我在这里解释的更清晰?
我同意“僵尸线程”的存在,这是一个术语,指的是与剩下的资源,他们不放手,但并没有完全死亡的线程发生了什么,因此名称“僵尸”,所以你的这个转介的解释是相当正确的钱!
2.僵尸线程出现在.NET上吗? (为什么?为什么不?)
是的,他们可以发生。 这是一个参考,实际上被Windows称为“僵尸”: MSDN使用死亡进程/线程字“僵尸”
经常发生这是另外一个故事,取决于你的编码技术和实践,至于你像线程locking,并做了一段时间,我甚至不担心发生在你身上的情况。
是的,正如@KevinPanko在评论中正确提到的那样,“僵尸线程”确实来自Unix,这就是为什么它们被用在XCode-ObjectiveC中,并被称为“NSZombie”并用于debugging。 它的行为几乎是一样的…唯一的区别是应该已经死亡的对象成为debugging的“僵尸对象”,而不是“僵尸线程”,这可能是您的代码中潜在的问题。
我可以很容易地制作僵尸线程。
var zombies = new List<Thread>(); while(true) { var th = new Thread(()=>{}); th.Start(); zombies.Add(th); }
这泄漏了线程句柄(对于Join()
)。 就被pipe理的世界而言,这只是另一个内存泄漏。
现在,杀死一个线程的方式,它实际上是锁在后面的痛苦,但可能的。 另一个人的ExitThread()
完成这项工作。 正如他发现的那样,文件句柄被gc清除了,但是围绕某个对象的lock
却不会。 但是,你为什么要这样做?