摘要: 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);
}
}
}
但没有什么是这么简单...... 使用这个类的代码在一个线程中运行,当然,对于测试,该应用程序是多线程的:
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”,因为它不再在函数的重新编写代码中引用,而这一次,它比“哈希”获取方法。所以我们最终得到的是一个零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 ......
抱歉
: - )
我无法在Linux / Ubuntu / Mono上重现问题,尽管在相同条件下使用相同的代码(多个相同的可执行文件同时运行,发布模式等)。
答案 0 :(得分:8)
这只是代码中的一个错误:终结者不应该访问托管对象。
实现终结器的唯一原因是释放非托管资源。在这种情况下,您应该仔细实施the standard IDisposable pattern。
使用此模式,您可以实现受保护的方法“protected Dispose(bool disposing)”。从终结器调用此方法时,它会清除非托管资源,但不会尝试清理托管资源。
在您的示例中,您没有任何非托管资源,因此不应该实现终结器。
答案 1 :(得分:3)
你所看到的是完全自然的。
你没有保留对拥有字节数组的对象的引用,因此对象(不是字节数组)实际上是免费的,垃圾收集器可以收集它。
垃圾收集器确实非常具有侵略性。
因此,如果您在对象上调用一个方法,该方法返回对内部数据结构的引用,并且对象的终结器搞乱了该数据结构,那么您还需要保持对该对象的实时引用。
垃圾收集器看到ex变量不再在该方法中使用,因此它可以,并且正如您所注意到的,将在适当的情况下(即时间和需要)进行垃圾收集。
执行此操作的正确方法是在ex上调用GC.KeepAlive,因此将此行代码添加到方法的底部,一切都应该很好:
GC.KeepAlive(ex);
我通过阅读Jeffrey Richter的书Applied .NET Framework Programming了解了这种侵略行为。
答案 2 :(得分:1)
这看起来像是工作线程和GC线程之间的竞争条件;为了避免它,我认为有两种选择:
(1)更改你的if语句使用ex.Hash [0]而不是res,这样ex就不能过早地GC了,或者
(2)在对Hash的调用期间锁定ex
这是一个相当漂亮的例子 - 教师的观点是,JIT编译器中可能存在一个只出现在多核系统上的错误,或者这种编码在垃圾收集方面会有微妙的竞争条件吗?
答案 3 :(得分:1)
我认为你所看到的是合理的行为,因为事情在多个线程上运行。这就是GC.KeepAlive()方法的原因,在这种情况下应该使用该方法告诉GC该对象仍在使用,并且它不是清理的候选对象。
查看“完整代码”响应中的DoWork函数,问题是紧接在这行代码之后:
byte[] res = ex.Hash;
该函数不再对 ex 对象进行任何引用,因此它有资格进行垃圾收集。将调用添加到GC.KeepAlive可以防止这种情况发生。
答案 4 :(得分:1)
对于在你的工作方法中被调用的终结器来说,这是完全不可能的 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确实......没有:)它是一个空的非inlinable / jittable方法,其唯一的目的是欺骗GC认为在此之后将使用该对象。
警告:如果DoWork方法是托管C ++方法,那么您的示例完全有效......如果您不希望析构函数是,那么 DO 必须手动保持托管实例处于活动状态从另一个线程中调用。 IE浏览器。您在最终确定时传递对将要删除一块非托管内存的托管对象的引用,并且该方法正在使用此相同的blob。如果不保持实例处于活动状态,那么GC和方法的线程之间就会出现竞争条件。
这最终会流泪。托管堆损坏......
答案 5 :(得分:1)
是的,这是一个issue come up before。
它更有趣,因为你需要为此发生这种情况,你最终会st st'''how how。。。。。。。。。。。。。。。。。。。。。。。。
答案 6 :(得分:1)
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 {
...
}
}
}
所以我们不能说'aC'可以在上面的代码中存活多久。在Other.work()完成之前,JIT可能会报告该引用。它可能会将Other.work()内联到其他方法中,并报告aC甚至更长。即使您在使用它之后添加“aC = null;”,JIT也可以自由地将此分配视为死代码并将其消除。无论JIT何时停止报告引用,GC都可能无法在一段时间内收集它。
担心最早可以收集aC这一点更有意思。如果你和大多数人一样,你会猜到最快的aC有资格收集是在Other.work()的“if”子句的结束时,我添加了评论。事实上,IL中不存在大括号。它们是您和您的语言编译器之间的语法契约。 一旦启动了对aC.m()的调用,Other.work()可以自由停止报告aC。
答案 7 :(得分:0)
您将在下面找到完整代码,从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();
}
}
对于有兴趣的人,我可以通过电子邮件发送压缩项目。