Implementing SyncLock using simpler language elements.
The other day I had to answer various questions about SyncLock.
What is really locked when you do SyncLock?
Why after doing SyncLock X, you can modify X and even assign to it a different value.
What are the performance implications of SyncLock?
After spending some time trying to answer SyncLock questions one after another, I figured that it would be easier just implement SyncLock using some simpler elements and leave the analysis of the code to ones who interested.
Let’s take a look at what compiler produces for this code:
=======================================================
Module Module1
Sub Main()
Dim o As New C1
SyncLock o
' inside the SyncLock
End SyncLock
End Sub
End Module
Class C1
End Class
=======================================================
Here is what we can see in ildasm when we open the exe and go to the Main (most interesting parts are blue).
=======================================================
.method public static void Main() cil managed
{
.entrypoint
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
// Code size 31 (0x1f)
.maxstack 1
.locals init ([0] class ConsoleApplication4.C1 o,
[1] class ConsoleApplication4.C1 _Vb_t_ref_0)
IL_0000: nop
IL_0001: newobj instance void ConsoleApplication4.C1::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: stloc.1
IL_0009: ldloc.1
IL_000a: call void [mscorlib]System.Threading.Monitor::Enter(object)
IL_000f: nop
IL_0010: nop
.try
{
IL_0011: leave.s IL_001c
} // end .try
finally
{
IL_0013: nop
IL_0014: ldloc.1
IL_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
IL_001a: nop
IL_001b: endfinally
} // end handler
IL_001c: nop
IL_001d: nop
IL_001e: ret
} // end of method Module1::Main
=======================================================
Note that there is quite a bit of action going on when we use simple SyncLock. Implementation of simple SyncLock appears to be quite larger than it is in the initial source code.
First note that compiler creates a temporary variable _Vb_t_ref_0 where it stores the lock object reference (o in our case) and then calls System.Threading.Monitor.Enter with this object.
The second interesting thing is the Try/Finally block.To make sure the object gets unlocked when code leaves SyncLock, compiler makes synthetic Try/Finally block and in the Finally it does System.Threading.Monitor.Exit with _Vb_t_ref_0
An interesting experiment would be creating VB code that produces similar IL. As you see a little sample is better that any description:
=======================================================
Module Module1
Sub Main()
Dim o As New C1
Dim _Vb_t_ref_0 As C1 = o
System.Threading.Monitor.Enter(_Vb_t_ref_0)
Try
' inside the SyncLock
Finally
System.Threading.Monitor.Exit(_Vb_t_ref_0)
End Try
End Sub
End Module
Class C1
End Class
=======================================================
To prove that this is how SyncLock works, let’s take a look at the IL by opening this in ildasm. As you see the IL for Main is almost identical to what we had before. The only difference is that _Vb_t_ref_0 and o have swapped positions as local[0] and local[1], but this does not make any difference to the nature of the code. :
=======================================================
.method public static void Main() cil managed
{
.entrypoint
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
// Code size 31 (0x1f)
.maxstack 1
.locals init ([0] class ConsoleApplication4.C1 _Vb_t_ref_0,
[1] class ConsoleApplication4.C1 o)
IL_0000: nop
IL_0001: newobj instance void ConsoleApplication4.C1::.ctor()
IL_0006: stloc.1
IL_0007: ldloc.1
IL_0008: stloc.0
IL_0009: ldloc.0
IL_000a: call void [mscorlib]System.Threading.Monitor::Enter(object)
IL_000f: nop
IL_0010: nop
.try
{
IL_0011: leave.s IL_001c
} // end .try
finally
{
IL_0013: nop
IL_0014: ldloc.0
IL_0015: call void [mscorlib]System.Threading.Monitor::Exit(object)
IL_001a: nop
IL_001b: endfinally
} // end handler
IL_001c: nop
IL_001d: nop
IL_001e: ret
} // end of method Module1::Main