我正在与一位队友讨论锁定在 .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.Enter
和 Monitor.Exit
被标记为 extern
。似乎可以想象 .NET 会执行某种处理来保护线程免受可能产生这种影响的系统组件的影响,但这纯粹是推测性的,可能只是基于我从未听说过“僵尸线程”这一事实前。所以,我希望我能在这里得到一些反馈:
是否有比我在这里解释的更清晰的“僵尸线程”定义?僵尸线程可以出现在 .NET 上吗? (为什么/为什么不?)如果适用,我如何强制在 .NET 中创建僵尸线程?如果适用,我如何利用锁定而不冒 .NET 中出现僵尸线程场景的风险?
更新
两年多前我问过这个问题。今天发生了这样的事情:
https://i.stack.imgur.com/qd6Wd.png
wait
或waitpid
。然后子进程被称为“僵尸进程”。另请参阅howtogeek.com/119815
是否有比我在这里解释的更清晰的“僵尸线程”定义?
对我来说似乎是一个很好的解释——一个线程已经终止(因此不能再释放任何资源),但其资源(例如句柄)仍然存在并且(可能)导致问题。
僵尸线程可以出现在 .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”文件的句柄,因此该文件将保持打开状态,直到程序终止(您可以使用进程资源管理器或类似工具进行检查)。 在调用 GC.Collect
之前,“test.txt”的句柄不会被释放——事实证明,创建一个泄漏句柄的僵尸线程比我想象的还要困难)
如果适用,我如何利用锁定而不冒 .NET 中出现僵尸线程场景的风险?
不要做我刚才做的事!
只要您的代码正确地自行清理(如果使用非托管资源,请使用 Safe Handles 或等效类),并且只要您不竭尽全力以奇怪而奇妙的方式杀死线程(最安全的方式是只是为了永不杀死线程 - 让它们正常终止自己,或者在必要时通过异常终止),您将拥有类似于僵尸线程的东西的唯一方法是如果某些东西出了非常错误(例如在 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 answer 中的锁定示例可能是当线程以不奇怪的方式终止时无法释放资源(在这种情况下为锁)的最简单方法,但这很容易通过使用 lock
语句来解决,或将版本放在 finally
块中。
也可以看看
C# IL codegen 的微妙之处在于一个非常微妙的情况,即使使用 lock 关键字,异常也会阻止锁被释放(但仅在 .Net 3.5 和更早版本中)
锁和异常不能混用
我已经清理了我的答案,但将原始答案留在下面以供参考
这是我第一次听说僵尸这个词,所以我假设它的定义是:
已终止但未释放其所有资源的线程
因此,鉴于该定义,是的,您可以在 .NET 中执行此操作,就像使用其他语言(C/C++、java)一样。
但是,我不认为这是不在 .NET 中编写线程、任务关键型代码的充分理由。可能还有其他理由决定反对 .NET,但是仅仅因为你可以拥有僵尸线程而注销 .NET 对我来说没有意义。僵尸线程在 C/C++ 中是可能的(我什至认为在 C 中更容易搞砸),许多关键的线程应用程序都在 C/C++ 中(大容量交易、数据库等)。
结论 如果您正在决定要使用的语言,那么我建议您考虑全局:性能、团队技能、日程安排、与现有应用程序的集成等。当然,僵尸线程是您应该考虑的事情,但由于与其他语言(如 C)相比,在 .NET 中实际犯此错误非常困难,我认为这种担忧将被上述其他事情所掩盖。祝你好运!
如果您没有编写正确的线程代码,原始答案僵尸†可能会存在。对于 C/C++ 和 Java 等其他语言也是如此。但这不是不在 .NET 中编写线程代码的理由。
就像使用任何其他语言一样,在使用某些东西之前要知道价格。它还有助于了解幕后发生的事情,以便您可以预见任何潜在的问题。
无论您使用哪种语言,任务关键系统的可靠代码都不容易编写。但我很肯定,在 .NET 中正确地编写代码并非不可能。同样 AFAIK,.NET 线程与 C/C++ 中的线程没有什么不同,它使用(或构建自)相同的系统调用,除了一些 .net 特定的构造(如 RWL 和事件类的轻量级版本)。
†我第一次听说僵尸这个词,但根据你的描述,你的同事可能是指一个线程在没有释放所有资源的情况下终止。这可能会导致死锁、内存泄漏或其他一些不良副作用。这显然是不可取的,但是因为这种可能性而单独挑出 .NET 可能不是一个好主意,因为它在其他语言中也是可能的。我什至认为在 C/C++ 中比在 .NET 中更容易搞砸(尤其是在没有 RAII 的 C 中),但是很多关键应用程序都是用 C/C++ 编写的,对吗?所以这真的取决于你的个人情况。如果您想从应用程序中提取每一盎司的速度并希望尽可能接近裸机,那么 .NET 可能不是最佳解决方案。如果您的预算很紧,并且需要与 Web 服务/现有的 .net 库/等进行大量交互,那么 .NET 可能是一个不错的选择。
extern
方法时,我不确定如何弄清楚幕后发生了什么。如果你有建议,我很想听听。 (2) 我同意在 .NET 中可以做到这一点。我想相信锁定是可能的,但是我今天还没有找到令人满意的答案来证明这一点
locking
是 lock
关键字,那么可能不是因为它序列化执行。为了最大限度地提高吞吐量,您必须根据代码的特征使用正确的构造。我想说如果你可以使用 pthreads/boost/thread pools/whatever 在 c/c++/java/whatever 中编写可靠的代码,那么你也可以用 C# 编写它。但是,如果您不能使用任何库以任何语言编写可靠的代码,那么我怀疑用 C# 编写会有所不同。
现在我的大部分答案已经被下面的评论纠正了。我不会删除答案,因为我需要声誉积分,因为评论中的信息可能对读者有价值。
Immortal Blue 指出,在 .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() { }
块时,finally
仍会在 ThreadAbortException
以这种方式被触发时执行。
事实证明,以下信息仅对 .NET 1 和 .NET 1.1 有效:
如果在 lock() { }
块内发生其他异常,并且 ThreadAbortException
恰好在 finally
块即将运行时到达,则不会释放锁。正如您所提到的,lock() { }
块编译为:
finally
{
if (lockWasTaken)
Monitor.Exit(temp);
}
如果另一个线程在生成的 finally
块内调用 Thread.Abort()
,则锁可能不会被释放。
lock()
但我看不到这个的任何用法......所以 - 这是如何连接的?这是 Monitor.Enter
和 Monitor.Exit
的“错误”用法(缺少 try
和 finally
的用法)
Monitor.Enter
和 Monitor.Exit
的错误使用,而没有正确使用 try
和 finally
- 无论如何,您的场景将锁定其他可能挂在 { 5},所以有一个死锁场景 - 不一定是僵尸线程......另外,你没有释放 Main
中的锁......但是,嘿......也许 OP 正在锁定死锁而不是僵尸 -线程:)
lock
或正确的 try
/finally
-usage 来避免任何“僵尸”线程
这与 Zombie 线程无关,但 Effective C# 一书中有一个关于实现 IDisposable 的部分(第 17 项),其中讨论了我认为您可能会感兴趣的 Zombie 对象。
我建议阅读这本书本身,但它的要点是,如果您有一个实现 IDisposable 或包含 Desctructor 的类,那么您应该做的唯一一件事就是释放资源。如果您在此处执行其他操作,则该对象有可能不会被垃圾收集,但也将无法以任何方式访问。
它给出了一个类似于下面的例子:
internal class Zombie
{
private static readonly List<Zombie> _undead = new List<Zombie>();
~Zombie()
{
_undead.Add(this);
}
}
当调用此对象的析构函数时,对自身的引用将放置在全局列表中,这意味着它在程序的整个生命周期内都保持活动状态并在内存中,但不可访问。这可能意味着资源(尤其是非托管资源)可能无法完全释放,这可能会导致各种潜在问题。
下面是一个更完整的示例。到达 foreach 循环时,Undead 列表中有 150 个对象,每个对象都包含一个图像,但该图像已被 GC 处理,如果您尝试使用它,则会出现异常。在此示例中,当我尝试对图像执行任何操作时,无论是尝试保存它,还是查看高度和宽度等尺寸,都会收到 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 中的僵尸,我想起了这一点,并认为其他人可能会觉得它很有趣!
_undead
是静态的吗?
NullReferenceException
,因为我觉得缺少的东西需要更多地与机器而不是应用程序相关联。这是正确的吗?
IDisposable
。我对终结器(析构函数)感到担忧,但仅仅拥有 IDisposable
不会使对象进入终结器队列并冒这种僵尸场景的风险。此警告涉及终结器,它们可能会调用 Dispose 方法。有些示例在没有终结器的类型中使用了 IDisposable
。情绪应该是资源清理,但这可能是不平凡的资源清理。 RX 有效地使用 IDisposable
来清理订阅,并且可以调用其他下游资源。 (顺便说一句,我也没有投反对票……)
在重负载下的关键系统上,编写无锁代码更好,主要是因为性能改进。看看 LMAX 之类的东西,以及它如何利用“机械同情”来对此进行精彩讨论。担心僵尸线程吗?我认为这是一个边缘情况,只是一个需要解决的错误,而不是不使用 lock
的充分理由。
听起来你的朋友更像是在向我炫耀他对晦涩的异国术语的了解!在我在 Microsoft UK 运行性能实验室的所有时间里,我从未在 .NET 中遇到过这个问题的实例。
lock
声明!
lock
块就可以了。在你知道你需要之前,我会避免为了性能优化而引入复杂性。
1.“僵尸线程”的定义是否比我在这里解释的更清晰?
我确实同意“僵尸线程”的存在,这是一个术语,指的是留下资源的线程会发生什么,他们不会放手但没有完全死亡,因此得名“僵尸”,所以你的这个转介的解释很对钱!
2. .NET 上会不会出现僵尸线程? (为什么/为什么不?)
是的,它们可能会发生。这是一个参考,实际上被 Windows 称为“僵尸”:MSDN uses the Word "Zombie" for Dead processes/threads
经常发生它是另一回事,并且取决于您的编码技术和实践,至于您喜欢线程锁定并且已经做了一段时间,我什至不会担心这种情况会发生在您身上。
是的,正如@KevinPanko 在评论中正确提到的那样,“僵尸线程”确实来自 Unix,这就是为什么它们在 XCode-ObjectiveC 中使用并被称为“NSZombie”并用于调试。它的行为方式几乎相同......唯一的区别是应该已经死亡的对象变成了用于调试的“ZombieObject”,而不是“Zombie Thread”,这可能是您代码中的潜在问题。
我可以很容易地制作僵尸线程。
var zombies = new List<Thread>();
while(true)
{
var th = new Thread(()=>{});
th.Start();
zombies.Add(th);
}
这会泄漏线程句柄(对于 Join()
)。就我们在托管世界中所关心的而言,这只是另一个内存泄漏。
现在,以一种实际上持有锁的方式杀死一个线程是一件痛苦的事,但有可能。另一个人的 ExitThread()
完成了这项工作。正如他发现的那样,文件句柄已被 gc 清理,但对象周围的 lock
不会。但你为什么要这样做?
ExitThread
调用有点怀疑。显然,它有效,但感觉更像是一个聪明的把戏,而不是一个现实的场景。我的目标之一是了解什么不应该做,这样我就不会意外地使用 .NET 代码创建僵尸线程。我可能已经知道从 .NET 代码调用已知会导致该问题的 C++ 代码会产生预期的效果。你显然对这些东西了解很多。您是否知道任何其他具有相同结果的情况(可能很奇怪,但还不够奇怪,不会无意发生)?Set excelInstance = GetObject(, "Excel.Application")