摘要: C#/.NET 应该是垃圾收集。C# 有一个析构函数,用于清理资源。当对象 A 在我尝试克隆其变量成员之一的同一行被垃圾收集时会发生什么?显然,在多处理器上,有时垃圾收集器会获胜......
问题
今天,在 C# 培训课程中,老师向我们展示了一些代码,这些代码只有在多处理器上运行时才会包含错误。
我会总结说,有时,编译器或 JIT 在从其调用的方法返回之前调用 C# 类对象的终结器会搞砸。
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,000,000 次 DoWork,垃圾收集器就会施展魔法,并尝试回收“ex”,因为它不再在函数的剩余代码中被引用,而这一次,它比“Hash”更快获取方法。所以我们最终得到的是一个零字节数组的克隆,而不是正确的(第一项在 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 上重现该问题