我和一个队友讨论了锁定.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.Enter和Monitor.Exit标记为extern。似乎可以想象,.NET做了某种处理,保护线程不受可能产生这种影响的系统组件的影响,但这纯粹是推测性的,可能只是基于我以前从未听说过"僵尸线程"这一事实。所以,我希望能在这里得到一些反馈:
"僵尸线"的定义是否比我在这里解释的更清楚?
.NET上是否可以出现僵尸线程?(为什么/为什么不呢?)
如果适用,如何在.NET中强制创建僵尸线程?
如果适用,如何在不冒.NET中僵尸线程场景的风险的情况下利用锁定?
更新
两年前我问过这个问题。今天发生了这样的事:
- 你确定你的搭档没有谈论僵局吗??
- @安德烈亚斯尼德曼-我知道什么是死锁,这显然不是滥用这个术语的问题。对话中提到了死锁,它明显不同于"僵尸线程"。对我来说,主要的区别在于死锁具有双向不可解析的依赖关系,而僵尸线程是单向的,并且需要终止的进程。如果你不同意,认为有更好的方法来看待这些事情,请解释
- 不一定…你也可以不释放一把锁…这基本上是相同的"循环"行为…但是的,你是真的…这更像是一种饥饿(en.wikipedia.org/wiki/resource_dillation):)
- @安德烈斯尼德米尔——我肯定认识到这种相似性,但我会犹豫是否称之为基本相同的。你会说死锁的定义和en.wikipedia.org/wiki/deadlock中的四个必要条件是不准确的吗?
- @安德烈斯尼德米尔-看起来确实更接近这个。他给我的例子涉及到一个建立套接字连接的锁定尝试失败——我对这个概念本身并不十分清楚(因此这个问题)。
- 我想这和"锁"没有关系。任何在没有释放资源(或以异常方式释放资源)的情况下崩溃的线程都应称为"僵尸线程"。所以在我的选择中,释放线程的异常方式就是原因。
- @查理-例子?
- 我认为术语"僵尸"实际上来自于Unix背景,就像在"僵尸进程"中一样,对吗????在Unix中有一个"僵尸进程"的明确定义:它描述了一个已经终止的子进程,但是子进程的父进程仍然需要通过调用wait或waitpid来"释放"子进程(及其资源)。子进程被称为"僵尸进程"。另请参见howtogeek.com/119815
- @Hogliux-Unix进程和.NET进程之间的对应关系是什么?
- 我的第一个想法是,它看起来确实像一个死锁,但通过阅读答案,我可以看到一些不同之处:)
- 这是"僵尸"一词的滥用。在Unix中,有一个进程处于Z状态。它仍然占据进程表中的一个条目,如果您有太多的条目,系统将无法再创建更多的进程。在.NET的这种情况下,有一个线程退出并将其留在孤立资源之后。螺纹不在Z状态,只是完成并消失了。(线程将一直留在内核中,直到它的所有句柄都关闭为止,在这种情况下,您可以将其描述为"僵尸"线程。)
- 从未见过。在多线程应用程序中可能出现的所有问题中,这个"僵尸"的问题在列表中排名靠后。如果devs停止不断地创建、终止和破坏线程,那么这将有助于解决这些问题。别这样,大家,求你们了,我求你们了!
- 如果程序的一部分崩溃,使程序处于未定义的状态,那么当然,这会导致程序的其余部分出现问题。如果在单线程程序中不正确地处理异常,也可能发生同样的事情。问题不在于线程,问题在于您具有全局可变状态,并且没有正确处理意外的线程终止。你的"非常聪明"的同事完全是基于这个。
- "自从第一台电脑出现以来,机器里就一直有幽灵。随机的代码段组合在一起形成意外的协议…"
- 我认为对标题的反复编辑已经足够多了。我不喜欢这里的Ops标题,但我想最好还是不要写了。锁上一个小时,暂时保持稳定。
- 我同意这是对术语"僵尸"的滥用;它使它看起来像unix z进程,但它具有误导性(显然是另一个问题)。此外,有一点他是绝对错误的:你不能使系统崩溃。你的过程?也许吧。系统?否。如果线程以非干净方式终止,则不会释放资源。但是,如果进程以非干净方式终止,那么操作系统确实会为您释放资源。
- 标题很好;"in.net"是一个明确的消除歧义的说法,即OP并不是在谈论Unix进程。
- 我们需要另一个问题"如何用僵尸线程破坏系统",当然有很多方法,你只需要学习directx(双关语)。
- 为什么这特定于.NET?你可以想象在其他语言中也有同样的问题…
- @乔:(1)网络不是一种语言。(2)它是特定于.NET的,因为我正在开发的应用程序是.NET,我询问的原因是实用的,而不是理论上的。这个一般性的问题当然适用于其他一些运行时环境,但它绝对不是任何语言的普遍关注点。有些语言的执行环境是如此抽象,以至于开发甚至可以对线程化完全不可知。在我学习的过程中,.NET的工作水平非常高,可以大大简化某些线程问题(例如僵尸线程)的安全性。
- 回答4:您只需要捕获zombiethreadexception来处理这个问题,很简单。:)因为僵尸天启还没有到来:p
- .NET源代码中已存在僵尸请参见referencesource.microsoft.com/q=zombie
对我来说,这似乎是一个很好的解释——一个线程已经终止(因此不能再释放任何资源),但其资源(例如句柄)仍然存在并且(可能)导致了问题。
- .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及更早版本中)也会被释放。
- 锁和异常不会混合
- 我记得当我使用后台工作人员在Excel中保存东西时,我并没有一直释放所有的资源(因为我只是跳过了调试等)。在随后的TaskManager中,我看到了大约50个Excel进程。我是否创建了僵尸xcelprocesses?
- @贾斯汀-+1-回答得很好。不过,我对你的电话有点怀疑。显然,这是可行的,但感觉更像是一个聪明的诡计,而不是一个现实的场景。我的目标之一是了解不该做什么,这样我就不会意外地用.NET代码创建僵尸线程。我可能已经知道,调用从.NET代码中引起这个问题的C++代码会产生预期的效果。你显然对这件事了解很多。你知道有没有其他有同样结果的案例(可能很奇怪,但还不够奇怪,不会无意中发生)?
- @贾斯汀-埃里克·利珀特的文章很有意义,我以前很明显读过(这是我问题中第二篇文章的一个摘录),但我不确定我以前是否很欣赏它,所以我现在要重读几次。你能想到其他可能会被偶然发现的边缘情况吗?
- @SmartCaveman你是100%正确的-ExitThread绝对不是一个现实的场景:)我不知道从.NET代码中泄漏句柄的任何方法,这些代码在某种程度上(例如通过第三方类间接调用P/Invoke)或.NET框架中的错误。
- @Justin-为了澄清我对您的理解是正确的,您是否同意将c lock关键字添加到任何代码块都可以保证不负责创建僵尸线程?(在我对Lippert的理解中,我是否正确地认识到,以前的C版本中唯一的这种情况是由一个并发线程在正确的时刻抛出一个ThreadAbortException?
- @smartcavenman是的,我认为你已经正确地理解了我——只要你使用lock关键字而不是显式地使用Monitor.Enter和Monitor.Exit,那么你就可以保证在线程终止时释放锁(除非发生了愚蠢的p/invoke),是的,对于以前的c版本来说,唯一这样的情况就是使用concur。租用线程调用Thread.Abort(据我所知)
- 所以答案是‘不在C 4’中,对吗?如果必须跳出clr才能获得僵尸线程,那么它看起来不像.NET问题。
- @贾斯汀-所以,这几乎让人安心,但现在我开始觉得我的队友说的话确实有些道理。我的想法是:(1)我们的应用程序是在ASP.NET上的;(2)ASP.NET有很多不同的可能情况,在这些情况下,它会抛出ThreadAbortException,并且执行路径可能会导致这种情况发生在每个请求的基础上;(3)在非常重的负载下,有很多请求(根据定义);
- (4)如果在每个请求的基础上也有一些地方发生了锁定,那么线程池中的一个线程在另一个线程从类似于Response.End()的东西中抛出ThreadAbortException的同时退出锁定的概率并不是很小,以至于不重要。你怎么认为?
- @smartcavenman当前线程抛出了ASP.NET抛出的ThreadAbortExceptions,所以我认为当输入Eric Lippert所说的锁时的争用条件不适用(如果代码是用优化编译的,也不会发生这种情况)。只有当另一个线程导致ThreadAbortException被引发时,该异常才能在尝试之前被抛出到no-op中。
- @贾斯汀-不确定我在跟踪。在ASP.NET中,有线程池,所以假设有10个线程(线程1,线程2…线程10)。假设线程3将要进入一个锁定的上下文,用Lazy
>
加载一些昂贵的查询,而就在线程7调用Response.End()之前(在没有操作空间的情况下)。为什么不符合条件?
- @斯特凡,我几乎肯定不会。我已经做了很多次了。Excel进程不是僵尸,因为它们仍在运行,但不能通过正常的UI立即访问。您应该能够通过调用getObject来检索它们。Set excelInstance = GetObject(,"Excel.Application")
- @因为Response.End只会中止线程7(Response.End调用的当前线程)。线程3将继续运行(并输入锁定的上下文)。如果线程3被中断,那么我唯一能想到的就是,代码库中可能有一些东西,当响应结束时,它会在其他线程上调用abort。
- 隐马尔可夫模型。。。为什么缺少using声明或讨论何时使用?
- 调用Abort()时,"test.txt"的句柄仍然关闭,作为file的终结器的一部分…—但是file的终结器不能保证在Abort()期间被调用,或者根本不能保证在这方面……
- "生成的僵尸线程将永远不会释放"test.txt"文件的句柄,因此该文件将保持打开状态,直到程序终止"不正确。小证明:`static void main(string[]args)new thread(gccollect.start();new thread(target).start();console.readline();private static void target()using(var file=file.open("test.txt",filemode.openorcreate))exitthread(0);private static void gccollect()while(true)thread.睡眠(10000);gc.collect();`
- @贾斯汀-我想我明白你在说什么了。我认为我们从Lippert的文章中不同地解释了这一行:"当刚刚获得锁的线程处于no op时,另一个线程可能导致线程中止异常。"我认为这意味着,由于一些不相关的原因,某些东西导致另一个正在执行的线程抛出了一个ThreadAbortException,并且这会以某种方式导致获取锁的线程永远不会释放锁。
- @Justin,我相信你解释的方式是,第二个线程将引用带锁的线程的一半,并在带锁的线程上调用thread.Abort(),而带锁的线程处于非操作状态。现在我想起来,这更有意义了。而且,它似乎与本文的其余部分以及您对我关于ASP.NET的问题的回答都一致。如果我们最终在同一页,请告诉我。
- 我打算冒险说——你的尝试很好,但你还是没能创建僵尸线程,因为这是不可能的。如果从示例中获取内存转储并查看正在运行的线程,您会发现线程不在那里;您会在堆中找到线程对象,因为由于main()中的gcRoot,活动引用仍处于活动状态,但OS线程会消失。像这里所有其他的答案一样-你没有提到一个简单的事实,这个概念上没有意义,也不能在.NET中发生。
我把我的答案整理了一下,但把原来的留作参考。
这是我第一次听说僵尸这个词,所以我假设它的定义是:
在不释放所有资源的情况下终止的线程
因此,给定定义,那么是的,您可以在.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可能是一个不错的选择。
- (1)我不知道如何弄清楚当我击中死胡同extern方法时引擎盖下发生了什么。如果你有什么建议,我想听听。(2)我同意在.NET中可以这样做。我想相信锁是可能的,但我至今还没有找到一个令人满意的答案来证明这一点。
- @如果您所说的locking是lock关键字,那么可能不是因为它序列化了执行。为了最大化吞吐量,您必须根据代码的特性使用正确的构造。我想说,如果你可以用C/S++/Java/任何使用Pthox/Booost /线程池/任何东西编写可靠的代码,那么你也可以用C语言编写它。但是如果你不能用任何一种语言使用任何库来编写可靠的代码,那么我怀疑用C编写代码会有什么不同。
- @聪明的穴居人,在弄清楚什么是在兜帽下,谷歌帮助堆,如果你的问题是太异国情调,在网络上找不到,反光镜是非常方便的。但是对于线程类,我发现msdn文档非常有用。很多都只是你在C-Anway中使用的相同系统调用的包装器。
- Jerahmeel,(1)如果你能在谷歌上找到一些东西,让这个看起来像LMGTFY问题,我马上支付你100美元。否则,请公平合理。(2)不能在extern方法上使用reflector。(3)我不知道C。你能推荐一本针对以前没有与C合作过的优秀程序员的好书/资源吗?
- @聪明的穴居人一点也不,这不是我的意思。如果是那样的话,我很抱歉。我想说的是,在编写关键应用程序时,您不应该太快地注销.NET。当然,你可以做一些比僵尸线程更糟糕的事情(我认为这只是没有释放任何非托管资源的线程,完全可以在其他语言中发生:stackoverflow.com/questions/14268080/…),但同样,这并不意味着.NET不是一个可行的解决方案。
- 我不认为.NET是一种糟糕的技术。它实际上是我的主要开发框架。但是,正因为如此,我认为理解它易受的缺陷是很重要的。基本上,我在挑选.net,但因为我喜欢它,而不是因为我不喜欢它(不用担心,兄弟,我已经教过你了)
- 这是不在.NET中编写线程代码的原因,但不是不使用.NET的原因。不要使用线程代码和显式锁,而是使用异步任务,它也提供了重叠和并行性,但这样做更容易理解和纠正问题。或任何其他消息传递体系结构。如果一次只有一个任务拥有一段数据,则不需要锁定(除了任务队列代码,您不必编写该代码)。
- 当然。在CPU密集型代码中,多线程应用程序至少释放了UI线程以保持对用户的响应,并允许用户执行多个并发任务。
现在我的大部分答案都被下面的评论纠正了。我不会删除答案,因为我需要信誉点,因为评论中的信息可能对读者有价值。
不朽蓝指出,在.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(),则不能释放锁。
- 你说的是lock(),但我看不出这有什么用…那么-这是怎么联系起来的?这是Monitor.Enter和Monitor.Exit的"错误"用法(缺少try和finally的用法)
- @示例应该回答问题2和3,我将更新答案。
- 我不会称之为僵尸线程-如果没有正确使用try和finally,这只是Monitor.Enter和Monitor.Exit的错误用法-无论如何,您的方案将锁定可能挂在_lock上的其他线程,因此存在死锁方案-不一定是僵尸线程…另外,您没有释放Main中的锁…但是,嘿…可能是OP正在锁定死锁,而不是僵尸线程:)
- @安德烈亚斯尼德曼也许对僵尸线的定义与我的想法不完全一致。也许我们可以将其称为"已终止执行但尚未释放所有资源的线程"。尝试删除并完成我的作业,但保留与OP方案相似的答案。
- 这里没有冒犯!事实上,你的回答让我想到了这一点:)因为OP明确地说锁没有被释放,我相信他的同事说了死锁——因为锁不是绑定到线程的真正资源(它是共享的……因此,通过坚持最佳实践并使用lock或适当的try或finally用法,可以避免任何"僵尸"线程。
- @安德烈斯尼德曼说,并避免埃多克斯1〔14〕。没有冒犯;我没有支持,就按照行动计划对僵尸的定义行事。
- 我必须纠正…饥饿:)(en.wikipedia.org/wiki/resource_饥荒)
- @我不确定我的定义是否准确。我问这个问题的部分原因是要澄清这个问题。我认为这个概念在C/C++中很有价值。
- @埃文豪斯-我很难准确理解你所说的,当你谈论到在锁里被抛出的异常时的意思。一般的想法是清楚的,但我不一定要在你写的其他代码的上下文中得到它,我也不完全确定你是指实际的lock语句,还是指你写的Monitor.Enter或Monitor.Exit代码。我想你是指lock声明,因为关于finally的说明,但请确认。
- @智能洞穴人看到我的最新答案。我确实是在谈论不执行finally街区的可能性。
- @c.evenhuis finally块不受线程中止异常的影响-请参阅social.msdn.microsoft.com/forums/en-us/…
- @不朽的蓝色谢谢你指出这一点。我的信息非常过时:"在.NET Framework 1.0和1.1版本中,在运行finally块时线程有可能中止,在这种情况下,finally块将中止。"msdn.microsoft.com/en-us/library/ty8d3wta(v=vs.110).aspx
- 请考虑更新/更正错误答案,而不是在评论中保留有价值的信息。尽管在开始时已经看过注释注释,但您的答案的其余部分的可见性比注释要高得多。或者,如果信息在其他答案中可用,则不需要在此处保存第二份。
这不是关于僵尸线程的,但是这本书有效的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中僵尸的,我被提醒这一点,并认为其他人可能会发现它很有趣!
- 这很有趣。_undead是静态的吗?
- 是的,对不起!
- 所以我尝试了一下,在"毁坏的"物体上打印出来。它将其视为普通对象。这样做有什么问题吗?
- 我用一个例子更新了我的答案,希望能更清楚地说明这个问题。
- 我不知道你为什么落选。我觉得它很有用。这里有一个问题-你会得到什么样的例外?我不希望它是一个NullReferenceException,因为我觉得丢失的东西需要更多地与机器联系,而不是与应用程序联系在一起。这是对的吗?
- 好吧,非常感谢,在回答中加入了例外,尽管我不完全理解为什么我会得到那个特别的例外!
- 当然,问题不在于IDisposable。我关心终结器(析构函数),但是仅仅拥有EDOCX1[2]不会使对象进入终结器队列并冒这个僵尸场景的风险。此警告涉及终结器,它们可能调用Dispose方法。例如,在没有终结器的类型中使用IDisposable。这种情绪应该是对资源的清理,但这可能是不平凡的资源清理。Rx有效地使用IDisposable清理订阅,并可以调用其他下游资源。(我也没有投反对票……)
在重负载的关键系统上,编写无锁代码主要是因为性能改进。看看像lmax这样的东西,以及它是如何利用"机械同情心"对此进行大量讨论的。但是担心僵尸线程吗?我认为这是一个边缘案例,只是一个需要解决的bug,并没有足够的理由不使用lock。
听起来更像你的朋友只是在幻想和炫耀他所知道的一个模糊的外来术语对我来说!在我运行微软英国性能实验室的所有时间里,我从未在.NET中遇到过这个问题的实例。
- 我认为不同的经验使我们对不同的错误产生了偏执的想法。他在解释的时候承认这是一个极端的边缘案例,但如果它确实是一个边缘案例而不是一个从不案例,我想至少更好地理解一点-谢谢你的意见
- 够公平的。我只是不希望你过分担心江户十一号声明!
- 如果我对这个问题有更深入的了解,我就不会那么担心了。
- 我投反对票是因为你想去掉一些线头,其中有太多。lock-fud需要做更多的工作来处理:)我只是在开发人员使用一个无限制的spinlock(为了性能)时畏缩,这样他们就可以将50K的数据复制到一个宽队列中。
- 通过性能改进,您的意思是锁定的实际成本吗?另外,您是在谈论.NET中的锁还是一般的锁?
- 是的,无论是一般的还是特殊的。编写正确的无锁代码和编程一样具有挑战性。在我的经验中,对于绝大多数情况,大的粗粒度(相对)易于理解的lock块是很好的。在您知道需要之前,我尽量避免为了性能优化而引入复杂性。
- +如果可以的话。关于正确的无锁代码,我之前为了在高流量的IIS系统中实现这一特定目的,必须优化一些非常关键的代码部分;我认为我所做的一切都没有更复杂或更奇怪的地方。另一方面,这段经历教会了我很多关于多线程和资源如何在.NET中工作的知识,我说:Bravo是这里唯一理智的声音。这里的每一个答案都是非常冗长和令人困惑的问题,因为这些问题不是众所周知的/研究过的。人们需要阅读苔丝·费尔南德斯的博客,或者避免评论GC。
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",用于调试的原因。它的行为几乎是一样的…唯一的区别是,一个本应终止的对象变成了用于调试的"僵尸对象",而不是"僵尸线程",这可能是代码中的一个潜在问题。
- 但是,msdn使用僵尸的方式与这个问题使用僵尸的方式非常不同。
- 哦,是的,我同意,但是我指出,即使是msdn在死后也将线程称为僵尸线程。它们实际上可以发生。
- 当然,但它指的是其他一些代码仍然拥有线程的句柄,而不是在退出时拥有资源句柄的线程。你的第一句话,同意问题的定义,是什么问题。
- 啊,我明白你的意思。老实说,我甚至没有注意到。我把重点放在他的观点上,即定义是存在的,不管它是如何发生的。记住,定义的存在是因为线程发生了什么,而不是它是如何完成的。
我可以很容易地制作僵尸线。
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]不会。但是为什么要这样做?