これは興味深いコード例だと思います。
クラスがありますTestと呼びましょうFinalizeメソッドを使用します。 Mainメソッドには、lockステートメントとMonitor.Enter()呼び出しを使用している2つのコードブロックがあります。また、ここにはTestクラスの2つのインスタンスがあります。実験は非常に簡単です。ロッキングブロック内のTest変数をnullにしてから、GC.Collectメソッド呼び出しで手動で収集してみます。したがって、Finalizeの呼び出しを確認するには、GC.WaitForPendingFinalizersメソッドを呼び出します。ご覧のとおり、すべてが非常に単純です。
lockステートメントの定義により、それはコンパイラによってtry {...} finally {..}ブロックに開かれます。 a Monitor.EntertryブロックおよびMonitor内で呼び出します。次に最終的にブロックで終了します。 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. test2がnull参照であるため、最後のブロックでnull参照例外。
私は驚き、コードを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によって収集されないが、使用する場合- Monitor.Enter収集して確定しますか?
これは、test1
が指す参照がILコードのローカル変数CS$2$0000
に割り当てられているためです。 C#でtest1
変数をnullにしますが、lock
構成は、個別の参照が維持されるようにコンパイルされます。
C#コンパイラがこれを行うのは実際にはかなり賢いです。そうでない場合、lock
ステートメントがクリティカルセクションの終了時にロックを強制的に解放することが保証されている保証を回避することが可能です。
LockステートメントとMonitor.Enter呼び出しの間に違いはありません。
もっと注意深く見てください。最初のケースでは、参照を2番目のローカル変数にコピーして、それが確実に存続するようにします。
C#3.0仕様がこの件について述べていることに注目してください。
"lock(x)..."という形式のロックステートメント(xは参照型の式)は、次のものとまったく同じです。
System.Threading.Monitor.Enter(x);
try { ... }
finally { System.Threading.Monitor.Exit(x); }
ただし、xは1回だけ評価されます。
それがその最後のビットです-xが1回しか評価されないことを除いて-これが動作の鍵です。 xが1回だけ評価されることを保証するために、1回評価し、結果をローカル変数に格納し、後でそのローカル変数を再利用します。
C#4では、codegenを変更して、
bool entered = false;
try {
System.Threading.Monitor.Enter(x, ref entered);
...
}
finally { if (entered) System.Threading.Monitor.Exit(x); }
繰り返しますが、xは評価済みのみです。プログラムでは、ロック式twiceを評価しています。あなたのコードは本当に
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ではEnterはinsideであり、C#3のように外側ではないことに注意してください。)