我想这是一个有趣的代码示例。
我们有一个类 - 我们称之为测试 - 使用 Finalize 方法。在 Main 方法中,有两个代码块,我使用的是lock语句和 Monitor.Enter()调用。另外,我在这里有两个测试类的实例。 实验非常简单:在锁定块中取消测试变量,然后尝试使用 GC.Collect 方法调用手动收集它。 因此,要查看 Finalize 调用,我将调用 GC.WaitForPendingFinalizers 方法。正如你所看到的,一切都很简单。
根据 lock 语句的定义,编译器将其打开到尝试 {...} 最终 {..}阻止,在尝试块内部进行 Monitor.Enter 调用,并使用监控。然后它退出 finally 块。我试图手动实现 try-finally 块。
我预计在两种情况下都会出现相同的行为 - 使用锁定和使用 Monitor.Enter 的行为。但是,令人惊讶的是,它有所不同,如下所示:
public class Test
{
private string name;
public Test(string name)
{
this.name = name;
}
~Test()
{
Console.WriteLine(string.Format("Finalizing class name {0}.", name));
}
}
class Program
{
static void Main(string[] args)
{
var test1 = new Test("Test1");
var test2 = new Test("Tesst2");
lock (test1)
{
test1 = null;
Console.WriteLine("Manual collect 1.");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Manual collect 2.");
GC.Collect();
}
var lockTaken = false;
System.Threading.Monitor.Enter(test2, ref lockTaken);
try {
test2 = null;
Console.WriteLine("Manual collect 3.");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Manual collect 4.");
GC.Collect();
}
finally {
System.Threading.Monitor.Exit(test2);
}
Console.ReadLine();
}
}
此示例的输出为:
手动收集1.手动收集2。 手动收集3.完成课程 名称Test2。手动收集4。 并且最后一个块中的null引用异常,因为test2是空引用。
我很惊讶并将我的代码反汇编成IL。所以,这是 Main 方法的IL转储:
.entrypoint
.maxstack 2
.locals init (
[0] class ConsoleApplication2.Test test1,
[1] class ConsoleApplication2.Test test2,
[2] bool lockTaken,
[3] bool <>s__LockTaken0,
[4] class ConsoleApplication2.Test CS$2$0000,
[5] bool CS$4$0001)
L_0000: nop
L_0001: ldstr "Test1"
L_0006: newobj instance void ConsoleApplication2.Test::.ctor(string)
L_000b: stloc.0
L_000c: ldstr "Tesst2"
L_0011: newobj instance void ConsoleApplication2.Test::.ctor(string)
L_0016: stloc.1
L_0017: ldc.i4.0
L_0018: stloc.3
L_0019: ldloc.0
L_001a: dup
L_001b: stloc.s CS$2$0000
L_001d: ldloca.s <>s__LockTaken0
L_001f: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&)
L_0024: nop
L_0025: nop
L_0026: ldnull
L_0027: stloc.0
L_0028: ldstr "Manual collect."
L_002d: call void [mscorlib]System.Console::WriteLine(string)
L_0032: nop
L_0033: call void [mscorlib]System.GC::Collect()
L_0038: nop
L_0039: call void [mscorlib]System.GC::WaitForPendingFinalizers()
L_003e: nop
L_003f: ldstr "Manual collect."
L_0044: call void [mscorlib]System.Console::WriteLine(string)
L_0049: nop
L_004a: call void [mscorlib]System.GC::Collect()
L_004f: nop
L_0050: nop
L_0051: leave.s L_0066
L_0053: ldloc.3
L_0054: ldc.i4.0
L_0055: ceq
L_0057: stloc.s CS$4$0001
L_0059: ldloc.s CS$4$0001
L_005b: brtrue.s L_0065
L_005d: ldloc.s CS$2$0000
L_005f: call void [mscorlib]System.Threading.Monitor::Exit(object)
L_0064: nop
L_0065: endfinally
L_0066: nop
L_0067: ldc.i4.0
L_0068: stloc.2
L_0069: ldloc.1
L_006a: ldloca.s lockTaken
L_006c: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&)
L_0071: nop
L_0072: nop
L_0073: ldnull
L_0074: stloc.1
L_0075: ldstr "Manual collect."
L_007a: call void [mscorlib]System.Console::WriteLine(string)
L_007f: nop
L_0080: call void [mscorlib]System.GC::Collect()
L_0085: nop
L_0086: call void [mscorlib]System.GC::WaitForPendingFinalizers()
L_008b: nop
L_008c: ldstr "Manual collect."
L_0091: call void [mscorlib]System.Console::WriteLine(string)
L_0096: nop
L_0097: call void [mscorlib]System.GC::Collect()
L_009c: nop
L_009d: nop
L_009e: leave.s L_00aa
L_00a0: nop
L_00a1: ldloc.1
L_00a2: call void [mscorlib]System.Threading.Monitor::Exit(object)
L_00a7: nop
L_00a8: nop
L_00a9: endfinally
L_00aa: nop
L_00ab: call string [mscorlib]System.Console::ReadLine()
L_00b0: pop
L_00b1: ret
.try L_0019 to L_0053 finally handler L_0053 to L_0066
.try L_0072 to L_00a0 finally handler L_00a0 to L_00aa
我认为 lock 语句与 Monitor.Enter 调用之间没有任何区别。 那么,为什么我仍然在 lock 的情况下引用 test1 的实例,并且该对象不是由GC收集的,而是在使用<的情况下strong> Monitor.Enter 它被收集并最终确定?
答案 0 :(得分:80)
我没有看到lock语句和Monitor.Enter调用之间有任何区别。
仔细看看。第一种情况将引用复制到第二个局部变量,以确保它保持活动状态。
请注意C#3.0规范中有关该主题的内容:
形式为“lock(x)...”的锁定语句,其中x是引用类型的表达式,恰好等同于
System.Threading.Monitor.Enter(x);
try { ... }
finally { System.Threading.Monitor.Exit(x); }
除了x只被评估一次。
最后一位 - 除了x只评估一次 - 这是行为的关键。为了确保仅在我们对其进行一次求值时对x进行求值,将结果存储在局部变量中,然后再重新使用该局部变量。
在C#4中,我们已经更改了codegen,现在它已经
了bool entered = false;
try {
System.Threading.Monitor.Enter(x, ref entered);
...
}
finally { if (entered) System.Threading.Monitor.Exit(x); }
但同样,x只是评估一次。在您的程序中,您正在评估锁定表达式两次。你的代码应该是
bool lockTaken = false;
var temp = test2;
try {
System.Threading.Monitor.Enter(temp, ref lockTaken);
test2 = null;
Console.WriteLine("Manual collect 3.");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Manual collect 4.");
GC.Collect();
}
finally {
System.Threading.Monitor.Exit(temp);
}
现在很清楚为什么它的运作方式呢?
(另请注意,在C#4中,输入在内部尝试,而不是在C#3之外。)
答案 1 :(得分:20)
这是因为test1
指向的引用被分配给IL代码中的局部变量CS$2$0000
。您在C#中清空了test1
变量,但lock
构造的编译方式使得维护单独的引用。
C#编译器实际上非常聪明。否则,有可能绕过保证lock
语句应该在退出临界区时强制释放锁。