Lock to tak naprawdę System.Threading.Monitor. Napiszmy, skompilujmy, zdekompilujmy i przeanalizujmy kod, w którym tworzymy sekcję krytyczną przy użyciu słówka kluczowego lock.
class TeoVincent
{
private readonly object _lockObj = new object();
public void Method()
{
lock (_lockObj)
{
Console.WriteLine("critical section");
}
}
}
Po skompilowaniu oraz otwarciu pliku wynikowego *.exe przez ILSpy, dotPeak lub inne tego typu narzędzie otrzymamy taki kod IL:
.method public hidebysig instance void
Method() cil managed
{
.maxstack 2
.locals init (
[0] object V_0,
[1] bool V_1
)
// [10 9 - 10 10]
IL_0000: nop
// [11 13 - 11 28]
IL_0001: ldarg.0 // this
IL_0002: ldfld object TeoVincent.Monitor.Enter.TeoVincent::_lockObj
IL_0007: stloc.0 // V_0
IL_0008: ldc.i4.0
IL_0009: stloc.1 // V_1
.try
{
IL_000a: ldloc.0 // V_0
IL_000b: ldloca.s V_1
IL_000d: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&)
IL_0012: nop
// [12 13 - 12 14]
IL_0013: nop
// [13 17 - 13 55]
IL_0014: ldstr "critical section"
IL_0019: call void [mscorlib]System.Console::WriteLine(string)
IL_001e: nop
// [14 13 - 14 14]
IL_001f: nop
IL_0020: leave.s IL_002d
} // end of .try
finally
{
IL_0022: ldloc.1 // V_1
IL_0023: brfalse.s IL_002c
IL_0025: ldloc.0 // V_0
IL_0026: call void [mscorlib]System.Threading.Monitor::Exit(object)
IL_002b: nop
IL_002c: endfinally
} // end of finally
// [15 9 - 15 10]
IL_002d: ret
} // end of method TeoVincent::Method
- linia 19: pojawił się blok
try{} - linia 23: mamy wywołanie metody
System.Threading.Monitor::Enter(object, bool&) - linia 38: mamy blok
finally{}. - linia 44: mamy wywołanie metody
System.Threading.Monitor::Exit(object)
Jak widać, podczas kompilacji generowany jest kod IL, który odpowiada takiej implementacji:
class TeoVincent
{
private readonly object _lockObj = new object();
public void Method()
{
try
{
System.Threading.Monitor.Enter(_lockObj);
Console.WriteLine("critical section");
}
finally
{
System.Threading.Monitor.Exit(_lockObj);
}
}
}
Przeanalizujmy teraz ten kod i zastanówmy się, czy jest on pozbawiony wad?
Jeśli wystąpi wyjątek w sekcji krytycznej wówczas trafiamy do bloku finally i zwolnimy zasób. Skoro powstał wyjątek, to możemy nie chcieć zwalniać sekcji krytycznej. Może lepszy byłby deadlock niż dalsze niszczenie danych przez wpuszczenie innego wątku.
Jeśli wyjątek wystąpi w metodzie Monitor.Enter, wówczas trafiamy do finally i wywołujemy metodę Monitor.Exit co spowoduje kolejny wyjątek.
Konstrukcja ta nie jest odporna na zakleszczenia. W szczególnych przypadkach mażemy chcieć sprawdzać czas, jaki dany wątek oczekuje na wejście do sekcji krytycznej. Po przekroczeniu zadanego czasu wątek odpuszcza sobie dalsze czekanie.
Rozwiązaniem problemów opisanych w punkcie dwóch ostatnich akapitach będzie kod:
class TeoVincent
{
private readonly object _lockObj = new object();
public void Method()
{
bool token = false;
try
{
System.Threading.Monitor.TryEnter(_lockObj, new TimeSpan(0, 0, 0, 5), ref token);
Console.WriteLine("critical section");
}
finally
{
if (token)
{
System.Threading.Monitor.Exit(_lockObj);
}
}
}
}
