Finalizer在其对象仍在使用时启动

总结: C#/ .NET应该是垃圾回收。 C#有一个析构函数,用于清理资源。 当一个对象A被垃圾收集在我试图克隆其变量成员之一的同一行时会发生什么? 显然,在多处理器上,垃圾收集器有时赢了……

问题

今天,在关于C#的培训课程中,老师向我们展示了一些仅在多处理器上运行时才包含错误的代码。

我将总结一下,有时候,编译器或JIT通过在从被调用方法返回之前调用C#类对象的终结器来搞砸。

Visual C ++ 2005文档中给出的完整代码将作为“答案”发布,以避免提出非常大的问题,但必要的内容如下:

以下类具有“Hash”属性,该属性将返回内部数组的克隆副本。 在构造中,数组的第一项值为2.在析构函数中,其值设置为零。

关键是:如果你试图获得“示例”的“哈希”属性,你将得到一个干净的数组副本,其第一个项目仍然是2,因为正在使用该对象(因此,不是垃圾收集/定稿):

public class Example { private int nValue; public int N { get { return nValue; } } // The Hash property is slower because it clones an array. When // KeepAlive is not used, the finalizer sometimes runs before // the Hash property value is read. private byte[] hashValue; public byte[] Hash { get { return (byte[])hashValue.Clone(); } } public Example() { nValue = 2; hashValue = new byte[20]; hashValue[0] = 2; } ~Example() { nValue = 0; if (hashValue != null) { Array.Clear(hashValue, 0, hashValue.Length); } } } 

但没有什么是这么简单的…使用这个类的代码在一个线程内部进行,当然,对于测试,该应用程序是multithreading的:

 public static void Main(string[] args) { Thread t = new Thread(new ThreadStart(ThreadProc)); t.Start(); t.Join(); } private static void ThreadProc() { // running is a boolean which is always true until // the user press ENTER while (running) DoWork(); } 

DoWork静态方法是问题发生的代码:

 private static void DoWork() { Example ex = new Example(); byte[] res = ex.Hash; // [1] // If the finalizer runs before the call to the Hash // property completes, the hashValue array might be // cleared before the property value is read. The // following test detects that. if (res[0] != 2) { // Oops... The finalizer of ex was launched before // the Hash method/property completed } } 

一旦每1,000,000个DoWork提供,显然,垃圾收集器会发挥其魔力,并试图回收“ex”,因为它不再在函数的重新编写代码中引用,而这次,它比“Hash”更快得到方法。 所以我们最终得到的是一个零ed字节数组的克隆,而不是正确的一个(第一项为2)。

我的猜测是代码的内联,它基本上取代了DoWork函数中标记为[1]的行:

  // Supposed inlined processing byte[] res2 = ex.Hash2; // note that after this line, "ex" could be garbage collected, // but not res2 byte[] res = (byte[])res2.Clone(); 

如果我们认为Hash2是一个简单的访问器,编码如下:

 // Hash2 code: public byte[] Hash2 { get { return (byte[])hashValue; } } 

所以,问题是: 这应该在C#/ .NET中以这种方式工作,还是可以将其视为JIT编译器的错误?

编辑

有关解释,请参阅Chris Brumme和Chris Lyons的博客。

http://blogs.msdn.com/cbrumme/archive/2003/04/19/51365.aspx
http://blogs.msdn.com/clyon/archive/2004/09/21/232445.aspx

每个人的回答都很有趣,但我不能选择一个比另一个好。 所以我给了你一个+1 …

抱歉

🙂

编辑2

我无法在Linux / Ubuntu / Mono上重现问题,尽管在相同条件下使用相同的代码(同时运行多个相同的可执行文件,发布模式等)

它只是代码中的一个错误:终结器不应该访问托管对象。

实现终结器的唯一原因是释放非托管资源。 在这种情况下,您应该仔细实现标准IDisposable模式 。

使用此模式,您可以实现受保护的方法“protected Dispose(bool disposing)”。 从终结器调用此方法时,它会清除非托管资源,但不会尝试清理托管资源。

在您的示例中,您没有任何非托管资源,因此不应该实现终结器。

你所看到的是完全自然的。

您没有保留对拥有字节数组的对象的引用,因此对象(不是字节数组)实际上是免费的,以便垃圾收集器收集。

垃圾收集器真的可以那么具有攻击性。

因此,如果在对象上调用一个方法,该方法返回对内部数据结构的引用,并且对象的终结器搞乱了该数据结构,则还需要保持对该对象的实时引用。

垃圾收集器看到ex变量不再在该方法中使用,因此它可以,并且正如您所注意到的,将在适当的情况下(即时间和需要)进行垃圾收集。

执行此操作的正确方法是在ex上调用GC.KeepAlive,因此将此行代码添加到方法的底部,并且一切都应该正常:

 GC.KeepAlive(ex); 

通过阅读Jeffrey Richter撰写的“ 应用.NET框架编程 ”一书,我了解了这种攻击性行为。

这看起来像是工作线程和GC线程之间的竞争条件; 为了避免它,我认为有两种选择:

(1)更改你的if语句使用ex.Hash [0]而不是res,这样ex就不能过早地GC了,或者

(2)在呼叫Hash期间锁定ex

这是一个非常漂亮的例子 – 老师的观点是,JIT编译器中可能存在一个只出现在多核系统上的错误,或者这种编码可能会有垃圾收集的微妙竞争条件?

我认为你看到的是合理的行为,因为事情是在多个线程上运行的。 这就是GC.KeepAlive()方法的原因,在这种情况下应该使用该方法告诉GC该对象仍在使用,并且它不是清理的候选对象。

查看“完整代码”响应中的DoWork函数,问题是紧跟在这行代码之后:

 byte[] res = ex.Hash; 

该函数不再对ex对象进行任何引用,因此它有资格进行垃圾收集。 将调用添加到GC.KeepAlive可以防止这种情况发生。

是的,这是以前出现的问题 。

它更加有趣,因为你需要为这种情况运行释放,你最终会st your”’,how how how???????????????

来自Chris Brumme博客的有趣评论

http://blogs.msdn.com/cbrumme/archive/2003/04/19/51365.aspx

 class C {
IntPtr _handle; Static void OperateOnHandle(IntPtr h) { ... } void m() { OperateOnHandle(_handle); ... } ... } class Other { void work() { if (something) { C aC = new C(); aC.m(); ... // most guess here } else { ... } } }

所以我们不能说’aC’可以在上面的代码中存活多久。 在Other.work()完成之前,JIT可能会报告该引用。 它可能会将Other.work()内联到其他方法中,并且报告aC甚至更长。 即使您在使用它之后添加“aC = null;”,JIT也可以自由地将此分配视为死代码并将其消除。 无论JIT何时停止报告引用,GC都可能无法收集它一段时间。

担心可以收集aC的最早期点更有意思。 如果你和大多数人一样,你会猜到最快的aC有资格收集是在Other.work()的“if”子句的结束时,我添加了评论。 实际上,IL中不存在大括号。 它们是您和您的语言编译器之间的语法契约。 一旦启动了对aC.m()的调用,Other.work()就可以自由停止报告aC。

在ex.Hash调用之后,在你的do work方法中调用终结器是完全正常的,CLR知道不再需要ex实例了…

现在,如果你想让实例保持活着,请执行以下操作:

 private static void DoWork() { Example ex = new Example(); byte[] res = ex.Hash; // [1] // If the finalizer runs before the call to the Hash // property completes, the hashValue array might be // cleared before the property value is read. The // following test detects that. if (res[0] != 2) // NOTE { // Oops... The finalizer of ex was launched before // the Hash method/property completed } GC.KeepAlive(ex); // keep our instance alive in case we need it.. uh.. we don't } 

GC.KeepAlive确实……没有:)它是一个空的非inlinable / jittable方法,其唯一的目的是欺骗GC认为在此之后将使用该对象。

警告:如果DoWork方法是托管C ++方法,则您的示例完全有效…如果您不希望从另一个线程中调用析构函数,则必须手动保持托管实例处于活动状态。 IE浏览器。 您传递对托管对象的引用,该托管对象将在最终确定时删除一块非托管内存,并且该方法正在使用此相同的blob。 如果不保持实例处于活动状态,那么GC和方法的线程之间就会出现竞争条件。

这最终会流泪。 托管堆腐败……

完整代码

您将在下面找到完整代码,从Visual C ++ 2008 .cs文件中复制/粘贴。 由于我现在在Linux上,没有任何Mono编译器或有关其使用的知识,我现在无法进行测试。 不过,几个小时前,我看到这段代码工作及其错误:

 using System; using System.Threading; public class Example { private int nValue; public int N { get { return nValue; } } // The Hash property is slower because it clones an array. When // KeepAlive is not used, the finalizer sometimes runs before // the Hash property value is read. private byte[] hashValue; public byte[] Hash { get { return (byte[])hashValue.Clone(); } } public byte[] Hash2 { get { return (byte[])hashValue; } } public int returnNothing() { return 25; } public Example() { nValue = 2; hashValue = new byte[20]; hashValue[0] = 2; } ~Example() { nValue = 0; if (hashValue != null) { Array.Clear(hashValue, 0, hashValue.Length); } } } public class Test { private static int totalCount = 0; private static int finalizerFirstCount = 0; // This variable controls the thread that runs the demo. private static bool running = true; // In order to demonstrate the finalizer running first, the // DoWork method must create an Example object and invoke its // Hash property. If there are no other calls to members of // the Example object in DoWork, garbage collection reclaims // the Example object aggressively. Sometimes this means that // the finalizer runs before the call to the Hash property // completes. private static void DoWork() { totalCount++; // Create an Example object and save the value of the // Hash property. There are no more calls to members of // the object in the DoWork method, so it is available // for aggressive garbage collection. Example ex = new Example(); // Normal processing byte[] res = ex.Hash; // Supposed inlined processing //byte[] res2 = ex.Hash2; //byte[] res = (byte[])res2.Clone(); // successful try to keep reference alive //ex.returnNothing(); // Failed try to keep reference alive //ex = null; // If the finalizer runs before the call to the Hash // property completes, the hashValue array might be // cleared before the property value is read. The // following test detects that. if (res[0] != 2) { finalizerFirstCount++; Console.WriteLine("The finalizer ran first at {0} iterations.", totalCount); } //GC.KeepAlive(ex); } public static void Main(string[] args) { Console.WriteLine("Test:"); // Create a thread to run the test. Thread t = new Thread(new ThreadStart(ThreadProc)); t.Start(); // The thread runs until Enter is pressed. Console.WriteLine("Press Enter to stop the program."); Console.ReadLine(); running = false; // Wait for the thread to end. t.Join(); Console.WriteLine("{0} iterations total; the finalizer ran first {1} times.", totalCount, finalizerFirstCount); } private static void ThreadProc() { while (running) DoWork(); } } 

对于那些感兴趣的人,我可以通过电子邮件发送压缩项目。