终结器在其对象仍在使用时启动
摘要:应该将C#/。NET进行垃圾收集。 Chas一个析构函数,用于清理资源。当对象A在我尝试克隆其变量成员之一的同一行中被垃圾回收时,会发生什么情况?显然,有时在多处理器上,垃圾收集器会胜出...
问题
今天,在C#培训课程中,老师向我们展示了一些仅在多处理器上运行时包含错误的代码。
我总结一下说,有时候,编译器或者JIT在从被调用方法返回之前,通过调用Cclass对象的终结器来搞砸。
Visual C ++ 2005文档中给出的完整代码将作为"答案"发布,以避免产生非常大的问题,但基本内容如下:
下列类具有"哈希"属性,该属性将返回内部数组的克隆副本。在构造时,数组的第一项值为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); } } }
但是没有什么事情如此简单...
使用此类的代码在一个线程内运行,当然,对于测试,该应用程序是多线程的:
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百万次DoWork执行一次,垃圾收集器就会发挥其魔力,并尝试回收" ex",因为该函数的剩余代码中不再引用它,这一次它比"哈希"更快获取方法。因此,最后我们得到的是零位字节数组的克隆,而不是拥有正确的字节数组(第一个项为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 ...
对不起
:-)
尽管在相同的条件下使用相同的代码(多个相同的可执行文件同时运行,发布模式等),但我无法在Linux / Ubuntu / Mono上重现该问题。
解决方案
完整代码
我们将在完整的代码下面找到,这些代码是从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(); } }
对于那些感兴趣的人,我可以通过电子邮件发送压缩后的项目。
这看起来像是工作线程和GC线程之间的竞争状态;为了避免这种情况,我认为有两种选择:
(1)将if语句更改为使用ex.Hash [0]而不是res,以便不能过早地对ex进行GC处理,或者
(2)在对Hash的调用期间锁定ex
那是一个非常漂亮的例子,这是老师的观点,即JIT编译器中可能存在只在多核系统上出现的错误,或者这种编码在垃圾收集中可能具有较弱的竞争条件?
由于事情在多个线程上运行,因此我认为我们看到的是合理的行为。这就是使用GC.KeepAlive()方法的原因,在这种情况下,应使用该方法来告知GC该对象仍在使用中,并且它不是清除对象。
查看"完整代码"响应中的DoWork函数,问题在于紧接此代码行:
byte[] res = ex.Hash;
该函数不再对ex对象进行任何引用,因此在那时它可以进行垃圾回收。将调用添加到GC.KeepAlive可以防止这种情况的发生。
在do work方法中调用finalizer非常正常,就像
ex.Hash调用,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不执行任何操作:)这是一个空的,不可插入的/ jittable方法,其唯一目的是诱使GC认为此后将使用该对象。
警告:如果DoWork方法是托管C ++方法,那么示例是完全有效的...如果我们不希望从另一个线程中调用析构函数,则必须手动保持托管实例处于活动状态。 IE。我们传递了对托管对象的引用,该对象将在完成时删除非托管内存的blob,并且该方法使用的是同一blob。如果我们不让该实例保持活动状态,则将在GC和方法的线程之间产生竞争状态。
而这最终会流下眼泪。并管理堆损坏...
我们所看到的是完全自然的。
我们没有保留对拥有字节数组的对象的引用,因此该对象(而非字节数组)实际上是免费的,以供垃圾回收器收集。
垃圾收集器确实可以那么积极。
因此,如果我们在对象上调用一个方法,该方法返回对内部数据结构的引用,并且对象的终结器使该数据结构混乱,那么我们还需要保留对该对象的实时引用。
垃圾收集器发现ex变量不再在该方法中使用,因此,我们可以注意到,它会在适当的情况下(例如,时间和需求)进行垃圾收集。
正确的方法是在ex上调用GC.KeepAlive,因此将以下代码行添加到方法的底部,一切应该都很好:
GC.KeepAlive(ex);
通过阅读Jeffrey Richter的《 Applied .NET Framework Programming》一书,我了解了这种激进行为。
是的,这是以前出现过的问题。
更加有趣的是,我们需要运行release才能实现此目的,并且最终昂首阔步:"呵呵,那怎么可能是null?"。
http://blogs.msdn.com/cbrumme/archive/2003/04/19/51365.aspx
class C {<br> 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 { ... } } }
So we can’t say how long ‘aC’ might live in the above code. The JIT might report the reference until Other.work() completes. It might inline Other.work() into some other method, and report aC even longer. Even if you add “aC = null;” after your usage of it, the JIT is free to consider this assignment to be dead code and eliminate it. Regardless of when the JIT stops reporting the reference, the GC might not get around to collecting it for some time. It’s more interesting to worry about the earliest point that aC could be collected. If you are like most people, you’ll guess that the soonest aC becomes eligible for collection is at the closing brace of Other.work()’s “if” clause, where I’ve added the comment. In fact, braces don’t exist in the IL. They are a syntactic contract between you and your language compiler. Other.work() is free to stop reporting aC as soon as it has initiated the call to aC.m().
这只是代码中的错误:终结器不应访问托管对象。
实现终结器的唯一原因是释放非托管资源。在这种情况下,我们应该仔细实现标准的IDisposable模式。
使用此模式,我们可以实现一个受保护的方法" protected Dispose(bool dispose)"。从终结器调用此方法时,它将清除非托管资源,但不会尝试清除托管资源。
在示例中,我们没有任何非托管资源,因此不应实现终结器。