Zine.net online

Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc
w Szukaj

mgrzeg.net - Admin on Rails :)

lock() internals

Mechanizmy wspierające tworzenie aplikacji wielowątkowych są obecne w .NET od zarania dziejów. Istnieją klasy opakowujące funkcje i obiekty systemowe, są także mechanizmy dostępne tylko w .NET i udostępnione w postaci wygodnych konstrukcji językowych. Jedną z takich konstrukcji obecnych w C# jest słowo kluczowe lock, które usprawnia synchronizację między wątkami praktycznie bez wpływu na wydajność. Lock przeszedł drobny lifting w wersji 4.0, jednak szczegóły omówimy na końcu.

Metoda 1: Kod haszowy & lock

Czas zatem przyjrzeć się nieco bliżej tej konstrukcji, a przy okazji może uda nam się odkryć kilka innych ciekawostek związanych z CLR :)
Zacznijmy od kawałka kodu w c#.

using System;
namespace pl.net.zine.Articles.WinDbg
{
 public class LockInternals
 {
    public static void Main(string[] args)
    {
     LockInternals li = new LockInternals();
     li.RunLockWithHashCode();
     li.RunLockOnly();
    }
    private void RunLockOnly()
    {
     object syncLockOnly = new object();
     Console.WriteLine("Before anything");
     Console.ReadKey();
     lock (syncLockOnly)
     {
       Console.WriteLine("Inside lock");
       Console.ReadKey();
     }
    }
    private void RunLockWithHashCode()
    {
     int hash = 0;
     object syncLockWithHash = new object();
     Console.WriteLine("Before anything");
     Console.ReadKey();
     hash = syncLockWithHash.GetHashCode();
     Console.WriteLine("After GetHashCode()");
     Console.ReadKey();
     lock (syncLockWithHash)
     {
       Console.WriteLine("Inside lock");
       Console.ReadKey();
     }
    }
 }
}
]

Przyjrzyjmy się w ildasmie bliżej prostszej nieco metodzie RunLockOnly

.method private hidebysig instance void  RunLockOnly() cil managed
{
 // Code size       64 (0x40)
 .maxstack  2
 .locals init ([0] object syncLockOnly,
          [1] object CS$2$0000)
 IL_0000:  nop
 IL_0001:  newobj     instance void [mscorlib]System.Object::.ctor()
 IL_0006:  stloc.0
 IL_0007:  ldstr      "Before anything"
 IL_000c:  call       void [mscorlib]System.Console::WriteLine(string)
 IL_0011:  nop
 IL_0012:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
 IL_0017:  pop
 IL_0018:  ldloc.0
 IL_0019:  dup
 IL_001a:  stloc.1
 IL_001b:  call       void [mscorlib]System.Threading.Monitor::Enter(object)
 IL_0020:  nop
 .try
 {
    IL_0021:  nop
    IL_0022:  ldstr      "Inside lock"
    IL_0027:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_002c:  nop
    IL_002d:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
    IL_0032:  pop
    IL_0033:  nop
    IL_0034:  leave.s    IL_003e
 }  // end .try
 finally
 {
    IL_0036:  ldloc.1
    IL_0037:  call       void [mscorlib]System.Threading.Monitor::Exit(object)
    IL_003c:  nop
    IL_003d:  endfinally
 }  // end handler
 IL_003e:  nop
 IL_003f:  ret
} // end of method LockInternals::RunLockOnly

Jak widać, kompilator rozwinął nam

lock (syncLockOnly)
{
 Console.WriteLine("Inside lock");
 Console.ReadKey();
}

do czegoś zbliżonego do:

System.Threading.Monitor.Enter(syncLockOnly);
try
{
 Console.WriteLine("Inside lock");
 Console.ReadKey();
}
finally
{
 System.Threading.Monitor.Exit(syncLockOnly);
}

A zatem tajemniczy i niewiele mówiący lock wykorzystuje klasę Monitor i jej statyczne metody Enter oraz Exit. Przyglądając się nieco bliżej klasie Monitor (korzystając np. z ILSpy) zauważamy, że nie wykorzystuje ona żadnych systemowych obiektów synchronizacyjnych poprzez P/Invoke (co czynią niektóre inne klasy, jak np. Mutex), a obie metody: Enter oraz Exit opatrzone są interesującym atrybutem

[MethodImpl(MethodImplOptions.InternalCall)]
public static extern void Enter(object obj);

Zgodnie z dokumentacją atrybut ten wskazuje na wywołanie metody zaimplementowanej wewnątrz CLR. Kiedyś wrócimy do tego, lecz teraz przejdźmy już do naszych przykładów z początku tekstu i oczywiście odpalmy WinDbg :)
Po uruchomieniu naszej przykładowej aplikacji z poziomu debugera zaczynamy standardowo od załadowania niezbędnych rozszerzeń

0:003> .loadby sos mscorwks
0:003> .load sosex

Puszczamy wykonanie programu i po dojściu do miejsca, kiedy pojawia się na konsoli napis

Before anything

zatrzymujemy wykonanie (ctrl+break w oknie debugera), przełączamy się na wątek główny

0:003> ~0s

aby następnie rzucić okiem na ramki stosu:

0:000> !mk
Thread 0:
*** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v2.0.50727_32\mscorlib\f58ab951b57c8526430486dcf7ee38fd\mscorlib.ni.dll
    ESP      EIP
00:U 0015eea4 76247468 KERNEL32!GetConsoleInput+0x15
01:U 0015eeac 76246c4d KERNEL32!ReadConsoleInputA+0x1a
02:U 0015eecc 0028b09c CLRStub[StubLinkStub]@170028b09c
03:M 0015ef54 524b2949 System.Console.ReadKey(Boolean)(+0x60 IL)(+0xa1 Native)
04:M 0015efdc 524b2837 System.Console.ReadKey()(+0x0 IL)(+0x7 Native)
05:M 0015efe0 00390183 *** WARNING: Unable to verify checksum for Articles.exe
pl.net.zine.Articles.WinDbg.LockInternals.RunLockWithHashCode()(+0x19 IL)(+0x73 Native)
06:M 0015f044 003900b4 pl.net.zine.Articles.WinDbg.LockInternals.Main(System.String[])(+0xd IL)(+0x44 Native)
07:U 0015f058 528e1b5c mscorwks!CallDescrWorker+0x33
08:U 0015f068 528f2209 mscorwks!CallDescrWorkerWithHandler+0xa3
09:U 0015f0e8 52906511 mscorwks!MethodDesc::CallDescr+0x19c
0a:U 0015f224 52906544 mscorwks!MethodDesc::CallTargetWorker+0x1f
0b:U 0015f240 52906562 mscorwks!MethodDescCallSite::CallWithValueTypes+0x1a
0c:U 0015f258 52970c45 mscorwks!ClassLoader::RunMain+0x223
0d:U 0015f3bc 52970b65 mscorwks!Assembly::ExecuteMainMethod+0xa6
0e:U 0015f624 529710b5 mscorwks!SystemDomain::ExecuteMainMethod+0x456
0f:U 0015faf4 5297129f mscorwks!ExecuteEXE+0x59
10:U 0015fb44 529711cf mscorwks!_CorExeMain+0x15c
11:U 0015fb8c 6e5a55ab mscoreei!_CorExeMain+0x38
12:U 0015fb98 70617f16 MSCOREE!ShellShim__CorExeMain+0x99
13:U 0015fba8 70614de3 MSCOREE!_CorExeMain_Exported+0x8
14:U 0015fbb0 761a3677 KERNEL32!BaseThreadInitThunk+0xe
15:U 0015fbbc 77df9f02 ntdll!__RtlUserThreadStart+0x70
16:U 0015fbfc 77df9ed5 ntdll!_RtlUserThreadStart+0x1b

Liczba poprzedzająca każdy wiersz określa numer ramki stosu, który możemy przekazać do kolejnego polecenia w celu bliższego przyjrzenia się interesującej nas ramce number five :)

0:000> !mdv 5
Frame 0x5: (pl.net.zine.Articles.WinDbg.LockInternals.RunLockWithHashCode()):
[A0]:this:0x027b387c (pl.net.zine.Articles.WinDbg.LockInternals)
[L0]:hash:0x00000000 (System.Int32)
[L1]:syncLockWithHash:0x027b3918 (System.Object)
[L2]:CS$2$0000:null (System.Object)

Z łatwością rozpoznajemy nasze zmienne lokalne: hash (int) oraz syncLockWithHash (object). Trzecia zmienna wskazuje na dodatkowy obiekt, który wykorzystywany jest przez kompilator do implementacji locka (vide kod IL).

Przyjrzyjmy się im zatem nieco bliżej:

0:000> !clrstack -a
OS Thread Id: 0x13e0 (0)
ESP       EIP    
[CIAP]
0015efe0 00390183 pl.net.zine.Articles.WinDbg.LockInternals.RunLockWithHashCode()
    PARAMETERS:
       this = 0x027b387c
    LOCALS:
       0x0015f014 = 0x00000000
       0x0015efe8 = 0x027b3918
       0x0015efe4 = 0x00000000
[CIAP]

Widzimy kilka zmiennych lokalnych, więc możemy pokusić się o bliższe przyjrzenie się im i porównanie z naszymi zmiennymi lokalnymi. Zacznijmy od ustawienia aktualnej ramki stosu na number five i zrzut wartości naszych zmiennych:

0:000> !mframe 5
0:000> !mframe
Current frame is 0x5.
0:000> !mdt hash
(System.Int32) VALTYPE (MT=52052d34, ADDR=0015f018)
    m_value:0x0 (System.Int32)
0:000> !mdt syncLockWithHash
027b3918 (System.Object)

Upewniamy się zatem, że pod adresem 0x027b3918 znajduje się nasz obiekt, który za chwileczkę będziemy b-lock-ować. Zanim przejdziemy dalej, zróbmy jeszcze jeden tajemniczy zrzut, który wyjaśnię za chwilę:

0:000> dd 0x027b3918 -0x4 l1
027b3914  00000000

oraz

0:000> !dumpobj 0x027b3918
Name: System.Object
MethodTable: 52050704
EEClass: 51de3ef0
Size: 12(0xc) bytes
(C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Object
Fields:
None

Zapamiętujemy wynik i wznawiamy wykonanie programu. Dochodzimy do miejsca, gdzie pojawia nam się komunikat

After GetHashCode()

i wskakujemy ponownie do debugera. Przełączamy się na wątek główny, ustawiamy bieżącą ramkę stosu i sprawdzamy zawartość stosu oraz zmiennych lokalnych

0:000> !mframe 5
0:000> !mdt hash
(System.Int32) VALTYPE (MT=52052d34, ADDR=0015f018)
    m_value:0x33c0d9d (System.Int32)
0:000> !mdt syncLockWithHash
027b3918 (System.Object)
0:000> !clrstack -a
OS Thread Id: 0x13e0 (0)
ESP       EIP    
[CIAP]
0015efe0 003901a9 pl.net.zine.Articles.WinDbg.LockInternals.RunLockWithHashCode()
    PARAMETERS:
       this = 0x027b387c
    LOCALS:
       0x0015f014 = 0x033c0d9d
       0x0015efe8 = 0x027b3918
       0x0015efe4 = 0x00000000
[CIAP]

po czym ponownie wykonujemy tajemniczy zrzut zawartości pamięci pod adresem okupowanym przez naszą zmienną syncLockWithHash:

0:000> dd 0x027b3918 -0x4 l1
027b3914  0f3c0d9d

Przypominamy sobie poprzednią zawartość (00000000) i dostrzegamy różnicę. Przyglądamy się przez chwilę zawartości, patrzymy na wartości zmiennych, znowu zawartość i … jakoś zawartość tej komórki bardzo przypomina wartość zmiennej hash!

hash:     0x033c0d9d
027b3914  0x0f3c0d9d

Nie jest to dokładnie ta sama wartość, ale bardzo zbliżona. Szczegóły różnicy wyjaśnimy później, jednak już teraz możemy przyjąć, że z jakiegoś powodu został tu wstawiony kod haszowy naszego obiektu.  Sprawdźmy jeszcze listę blokad:

0:000> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
-----------------------------

Pusto.
Puszczamy dalej wykonanie programu i zatrzymujemy się po pojawieniu się komunikatu:

Inside lock

Sprawdzamy zawartość stosu

0:000> !clrstack -a
OS Thread Id: 0x13e0 (0)
ESP       EIP    
[CIAP]
0015efe0 003901ce pl.net.zine.Articles.WinDbg.LockInternals.RunLockWithHashCode()
    PARAMETERS:
       this = 0x027b387c
    LOCALS:
       0x0015f014 = 0x033c0d9d
       0x0015efe8 = 0x027b3918
       0x0015efe4 = 0x027b3918
[CIAP]

Jak widać, teraz obie zmienne wskazują na ten sam obiekt (dup w kodzie IL), co jednak nie wpływa na zawartość samego obiektu. Dalej zaglądamy tu i tam:

0:000> dd 0x027b3918 -0x4 l1
027b3914  08000002
0:000> !dumpobj 0x027b3918
Name: System.Object
MethodTable: 52050704
EEClass: 51de3ef0
Size: 12(0xc) bytes
(C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Object
Fields:
None
0:000> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
    2 005d890c            1         1 005b9620  13e0   0   027b3918 System.Object
-----------------------------

Ze zdumieniem zauważamy, że zawartość komórki zmieniła się i zamiast poprzedniego kodu haszowego zawiera teraz liczbę 08000002.
Podobnie wynik działania polecenia syncblk również jest inny od poprzedniego - pojawia nam się blokada o indeksie 2, którą łatwo wiążemy z naszym obiektem (kolumna SyncBlock i adres 027b3918).

Przechodzimy kilka kroków i sprawdzamy jeszcze zawartość tuż po wyjściu z locka. Okazuje się, iż:

0:000> dd 0x027b3918 -0x4 l1
027b3914  08000002

czyli identycznie jak wewnątrz locka, oraz

0:000> !syncblk 2
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
    2 005d890c            0         0 00000000     none    027b3918 System.Object
-----------------------------

Metoda 2: Bez wyznaczania kodu haszowego

Zanim rozwiążemy wszystkie zagadki, sprawdźmy działanie drugiej metody i porównajmy z uzyskanymi wcześniej wynikami. Jedyna bowiem różnica pomiędzy obiema metodami polega na tym, iż w przypadku pierwszej wyznaczaliśmy kod haszowy obiektu przed założeniem blokady, a w przypadku drugiej - nie. Przechodząc kolejno drugą metodę i zatrzymując się w tych samych miejscach otrzymujemy:

1. Przed założeniem blokady (dla czytelności pomijam w opisie kilka kroków, które są analogiczne do tych z poprzedniej części)

0:000> !mdt syncLockOnly
027b45cc (System.Object)
0:000> dd 0x027b45cc-0x4 l1
027b45c8  00000000
0:000> !dumpobj 027b45cc
Name: System.Object
MethodTable: 52050704
EEClass: 51de3ef0
Size: 12(0xc) bytes
(C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Object
Fields:
None
0:000> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
-----------------------------
0:000> !syncblk 2
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
    2 005d890c            0         0 00000000     none    027b3918 System.Object
-----------------------------

Z ostatniego zrzutu wynika, że ciągle poniewiera się gdzieś nasz obiekt z poprzedniej metody :)

2. Po założeniu blokady

0:000> dd 0x027b45cc-0x4 l1
027b45c8  00000001
0:000> !dumpobj 0x027b45cc
Name: System.Object
MethodTable: 52050704
EEClass: 51de3ef0
Size: 12(0xc) bytes
(C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Object
Fields:
None
ThinLock owner 1 (005b9620), Recursive 0
0:000> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
                                     PreEmptive   GC Alloc           Lock
      ID OSID ThreadOBJ    State     GC       Context       Domain   Count APT Exception
  0    1 13e0 005b9620      a020 Enabled  027b4630:027b5fe8 00583c88     1 MTA
  2    2  470 005ca388      b220 Enabled  00000000:00000000 00583c88     0 MTA (Finalizer)
0:000> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
-----------------------------

A zatem zawartość komórki 027b45c8 zmieniła się z 00000000 na 00000001. Dodatkowo, na samym końcu zrzutu obiektu otrzymaliśmy informację

ThinLock owner 1 (005b9620), Recursive 0

co po wykonaniu dodatkowo polecenia threads informuje nas o wątku, który trzyma blokadę. Co ciekawe jednak, syncblk nie zwraca nam żadnego wyniku - zupełnie jakby blokady w ogóle nie było!

O co w tym wszystkim chodzi?!

Czas na rozwiązanie zagadek. Zacznijmy od tego, jak jest skonstruowany obiekt zapisany na zarządzanej stercie. Otóż każdy obiekt składa się z 3 podstawowych elementów:

(1) Blok synchronizacji - to właśnie jego przeglądaliśmy wykonując tajemnicze zrzuty

0:000> dd 0x027b3918 -0x4 l1
027b3914  0f3c0d9d

w których otrzymywaliśmy różne wartości (wyjaśnienie dalej)

(2) Uchwyt typu - zawiera adres tabeli metod danego obiektu. Wykonując zrzut stosu, tudzież podglądając zawartość zmiennych lokalnych dostawaliśmy adres pamięci, gdzie przechowywany jest właśnie uchwyt typu. Wykonując zrzut zawartości pamięci dla obiektu z drugiej metody:

0:000> dd 0x027b45cc
027b45cc  52050704 00000000 00000000 52051718
027b45dc  00000011 00650042 006f0066 00650072
027b45ec  00610020 0079006e 00680074 006e0069
027b45fc  000d0067 0000000a 00000000 52051718
027b460c  0000000d 006e0049 00690073 00650064
027b461c  006c0020 0063006f 000d006b 0000000a
027b462c  00000000 00000000 00000000 00000000
027b463c  00000000 00000000 00000000 00000000

widzimy, że tabela metod znajduje się pod adresem 0x52050704. Wykonując dodatkowy zrzut tabeli metod dla tego adresu:

0:000> !dumpmt -md 52050704
EEClass: 51de3ef0
Module: 51de1000
Name: System.Object
mdToken: 02000002  (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 14
--------------------------------------
MethodDesc Table
  Entry MethodDesc      JIT Name
51fa6a90   51e2493c   PreJIT System.Object.ToString()
51fa6ab0   51e24944   PreJIT System.Object.Equals(System.Object)
51fa6b20   51e24974   PreJIT System.Object.GetHashCode()
520174c0   51e24998   PreJIT System.Object.Finalize()
520037d0   51e24934   PreJIT System.Object..ctor()
51f61d7c   51e2498c     NONE System.Object.GetType()
51f61d8c   51e249a0     NONE System.Object.MemberwiseClone()
51f6741c   51e249ac   PreJIT System.Object.FieldSetter(System.String, System.String, System.Object)
51f6742c   51e249b8   PreJIT System.Object.FieldGetter(System.String, System.String, System.Object ByRef)
51f6743c   51e249c4   PreJIT System.Object.GetFieldInfo(System.String, System.String)
51f61d68   51e2494c     NONE System.Object.InternalEquals(System.Object, System.Object)
51fa6ad0   51e2495c   PreJIT System.Object.Equals(System.Object, System.Object)
51fa6b00   51e24968   PreJIT System.Object.ReferenceEquals(System.Object, System.Object)
51f61d70   51e2497c     NONE System.Object.InternalGetHashCode(System.Object)

otrzymujemy pełną informację o analizowanym obiekcie.

(3) Właściwe dane obiektu - znajdują się tuż za uchwytem do typu.

Gdy już wiemy jak jest skonstruowany obiekt, który trafia na zarządzaną stertę, wyjaśnijmy co się dzieje z blokiem synchronizacji w obu analizowanych przypadkach.
Na początku pierwszej metody, tuż po utworzeniu obiektu syncLockWithHash, blok synchronizacji był pusty:

0:000> dd 0x027b3918 -0x4 l1
027b3914  00000000

Bezpośrednio po wykonaniu metody GetHashCode() blok synchronizacji został wypełniony kodem haszowym obiektu (033c0d9d), wraz z maską 0x0c000000, wyjaśnienie której pozostawimy sobie na później.

0:000> dd 0x027b3918 -0x4 l1
027b3914  0f3c0d9d

Chwilę później założyliśmy blokadę i wówczas blok synchronizacji zawierał:

0:000> dd 0x027b3918 -0x4 l1
027b3914  08000002

z czego maska 08000000 oznacza, iż został utworzony odpowiedni wpis w tabeli bloku synchronizacji, która jest pilnie strzeżona przez CLR i do której nie ma bezpośredniego dostępu. Liczba 0002 wskazywała na indeks w tabeli bloku synchronizacji, który mogliśmy przekazać jako parametr do polecenia syncblk. Więcej informacji możemy znaleźć w źródłach do referencyjnej implementacji .NET Framework - rotora. Oczywiście to nie są źródła dystrybuowanego .NET, jednak rzucają sporo światła na całość. Zajrzyjmy do pliku sscli20\clr\src\vm\syncblk.h (127):

#define BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX    0x08000000
// if BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX is clear, the rest of the header dword is layed out as follows:
// - lower ten bits (bits 0 thru 9) is thread id used for the thin locks
//   value is zero if no thread is holding the lock
// - following six bits (bits 10 thru 15) is recursion level used for the thin locks
//   value is zero if lock is not taken or only taken once by the same thread
// - following 11 bits (bits 16 thru 26) is app domain index
//   value is zero if no app domain index is set for the object

W przypadku drugiej metody, w której nie wypełnialiśmy bloku synchronizacji kodem haszowym obiektu, blok synchronizacji zmienił swoją wartość po założeniu blokady na 00000001 i mamy do czynienia z sytuacją opisaną wyżej:

0:000> dd 0x027b45cc-0x4 l1
027b45c8  00000001

okazało się, że nie został dodany dodatkowy wpis w tabeli bloku synchronizacji, utworzona została natomiast blokada lekka, której obecność zdradził nam zrzut obiektu

0:000> !dumpobj 0x027b45cc
[CIAP]
ThinLock owner 1 (005b9620), Recursive 0

Jak widać, CLR ma do dyspozycji wiele sposobów na utrzymanie informacji o stanie obiektu synchronizującego i zupełnie niepozorone wykonanie metody GetHashCode() może zmienić całkowicie stan rzeczy :)
Spytacie pewnie, czy trzymanie kodu haszowego w bloku synchronizacji ma sens, a ja wam odpowiem - a i owszem! Funkcja haszująca wcale nie musi być szybka, więc zapisanie raz wyliczonego haszu może być całkiem dobrym pomysłem - zgodnie z założeniami funkcja haszująca powinna korzystać z takich elementów obiektu, które nie powinny się zmieniać z czasem, a w sumie powinny dawać względnie stochastyczny rozkład, gdzie dany zbiór obiektów powinien być mniej-więcej równomiernie rozłożony (oczywiście powtórzenia są jak najbardziej dopuszczalne!).

Pozostaje nam jeszcze kwestia różnic implementacyjnych locka w wersji 4.0 .NET vs wcześniejsze wersje.
Otóż w najnowszej, 4 wersji nasza funkcja RunLockWithHashCode() ma następującą postać w kodzie IL:

.method private hidebysig instance void RunLockWithHashCode() cil managed
{
// Code size 104 (0x68)
.maxstack 2
.locals init (int32 V_0,
object V_1,
bool V_2,
object V_3,
bool V_4)
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: newobj instance void [mscorlib]System.Object::.ctor()
IL_0008: stloc.1
IL_0009: ldstr "Before anything"
IL_000e: call void [mscorlib]System.Console::WriteLine(string)
IL_0013: nop
IL_0014: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_0019: pop
IL_001a: ldloc.1
IL_001b: callvirt instance int32 [mscorlib]System.Object::GetHashCode()
IL_0020: stloc.0
IL_0021: ldstr "After GetHashCode()"
IL_0026: call void [mscorlib]System.Console::WriteLine(string)
IL_002b: nop
IL_002c: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_0031: pop
IL_0032: ldc.i4.0
IL_0033: stloc.2
.try
{
IL_0034: ldloc.1
IL_0035: dup
IL_0036: stloc.3
IL_0037: ldloca.s V_2
IL_0039: call void [mscorlib]System.Threading.Monitor::Enter(object,
bool&)
IL_003e: nop
IL_003f: nop
IL_0040: ldstr "Inside lock"
IL_0045: call void [mscorlib]System.Console::WriteLine(string)
IL_004a: nop
IL_004b: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
IL_0050: pop
IL_0051: nop
IL_0052: leave.s IL_0066
} // end .try
finally
{
IL_0054: ldloc.2
IL_0055: ldc.i4.0
IL_0056: ceq
IL_0058: stloc.s V_4
IL_005a: ldloc.s V_4
IL_005c: brtrue.s IL_0065
IL_005e: ldloc.3
IL_005f: call void [mscorlib]System.Threading.Monitor::Exit(object)
IL_0064: nop
IL_0065: endfinally
} // end handler
IL_0066: nop
IL_0067: ret
} // end of method LockInternals::RunLockWithHashCode

co sprowadza się mniej więcej do:

bool locked = false;
try
{
 Monitor.Enter(syncLockWithHash, ref locked);
 Console.WriteLine("Inside lock");
 Console.ReadKey();
}
finally
{
 if (locked)
    Monitor.Exit(syncLockWithHash);
}

Zmiana nie jest duża, chodziło jednak o wyeliminowanie sytuacji, w której mogło dojść do czegoś nieoczekiwanego między wywołaniem Monitor.Enter(object) a początkiem bloku try (np. w wyniku wyjątku OutOfMemoryException, etc.) i teraz to metoda Monitor.Enter decyduje, czy blokada jest już założona i zależnie od tego wykonywana jest dalej Monitor.Exit, lub nie. W końcu wątek mógł zostać wywłaszczony w dowolnym momencie, a co się w międzyczasie może wydarzyć, jeden czort tylko wie :)

Sprawę GetHashCode i masek bitowych w bloku synchronizacji, a także CLR-owy InternalCall pozostawiam na któryś następny wpis, odnoszę bowiem wrażenie, że to byłoby za dużo dobrego jak na jeden raz :)

Opublikowane 8 kwietnia 2011 22:15 przez mgrzeg
Filed under: ,

Powiadamianie o komentarzach

Jeżeli chciałbyś otrzymywać email gdy ta wypowiedź zostanie zaktualizowana, to zarejestruj się tutaj

Subskrybuj komentarze za pomocą RSS

Komentarze:

 

dotnetomaniak.pl said:

Dziękujemy za publikację - Trackback z dotnetomaniak.pl

kwietnia 9, 2011 00:04
 

PH said:

Bardzo dobry i ciekawie opowiedziany wpis ;)

Myślę, że warto jeszcze w tym miejscu nadmienić o jednym dość istotnym problemie związanym ze słowem "lock" i reakcją na wyjątek rzucany przez kod z jego wnętrza.

Niestety ani poprzednie wersje .NETa, ani zmiany wprowadzone w najnowszsej nie wyeliminowały go. Co ciekawe też mało programistów jest "uświadomionych", co do krytycznych konsekwencji tego wyjątku. W tym momencie od razu nasuwa się odpowiedź - "ale przecież blokada została zdjęta, wszystko jest OK, mamy nawet sprawdzanie, czy ją wcześniej założyliśmy, więc o co mu chodzi?".

Tak, zdjęta - no właśnie, w połowie aktualizacji naszych krytycznych danych, które owinięte były w "lock"? Tym samym też zezwoliliśmy innemu wątkowi na dostęp do nich (czytaj: do stanu nieustalonego!), co może (i prawdopodobnie będzię miało) implikację w postaci kolejnego wyjątku, w innej części systemu, lub co gorsza niezauważonej propagacji nieprawidłowego stanu dalej w aplikacji.

Co można na to poradzić?

- Jeśli potrzebujemy zmienić pojedynczą wartość, to funkcje "Interlocked" będą odpowiedniejsze (i również zdecydowanie szybsze) - może kiedyś zobaczymy tutaj też posta Michała i o tym :)

- proponowałbym też zamiast słowa "lock", rozwinąć samemu jego postać na wywołanie Monitor.Enter / Monitor.Exit i nie zwalniać blokady, przy wyjątku; a nawet dodać kod uruchamiający debugger; bądź, co bądź wyjątek jest rzeczą, której się nie spodziewamy, warto zatem zainteresować się jego wystąpieniem tutaj;

- kategorycznie nie wywoływać funkcji wirtualnych, ani innych delegatów z wnętrza "lock" (co też wiele razy widziałem), jeśli już tak jesteśmy przywiązani do tej konstrukcji

Ktoś ma jakiś lepszy pomysł?

Pozdrawiam!

kwietnia 9, 2011 01:52
 

Paweł said:

Michale, proszę o kilka słów wyjaśnień dla prostego programisty:

Jaka jest praktyczna różnica pomiędzy lockiem założonym na obiekcie, a lockiem założonym na jego HashCode?

Czy są sytuacje, w których lepiej uciec się do tej drugiej konstrukcji?

Pozdrawiam.

kwietnia 9, 2011 22:01
 

mgrzeg said:

@PH: Pawel, dzieki za kolejne dobre slowo. Lock jest wzglednie lekkim rozwiazaniem, glownie za sprawa tego, ze nie nastepuje przelaczanie do trybu jadra i w porownaniu z innymi rozwiazaniami 'ze swojej klasy' wypada calkiem dobrze. Obok Interlocked jest jeszcze kilka lekkich nowych rozwiazan - jak chociazby struktura SpinLock, czy SpinWait, ktore pewnie ktos kiedys opisze ;)

@Paweł: Jakkolwiek rozwiazanie 'headerowe' (czyli bez wolania jawnie GetHashCode) jest lzejsze i opiera sie na spin locku, to jednak moze w pewnym momencie nastapic wymuszenie utworzenia bloku synchronizacji i wowczas z calej optymalizacji 'nici' (petla oczekujaca nie jest nieskonczona, tylko ograniczona). Tak wiec w moim odczuciu spokojnie mozna to potraktowac jako 'szczegol implementacyjny', choc w przypadku krotkiej sekcji krytycznej (powiedzmy kilkadziesiat instrukcji - przelaczenie do trybu jadra to ok. 1000 instrukcji, wiec tu musi byc zdecydowanie mniej) moze sie okazac, ze warto pamietac o tym szczegole :)

Nie napisalem tego w tekscie, jednak blok synchronizacyjny tworzony jest rowniez przy okazji interoperacyjnosci, ktorej informacje rowniez przechowywane sa w naglowku obiektu i podobnie jak w przypadku kodu haszowego podczas blokowania przenoszone sa w wewnetrzne struktury CLR, tak wiec GetHashCode to nie jedyny przyczynek do tworzenia bloku synchronizacyjnego. I tak jak napisal PH - warto czasem pomyslec nad innymi obiektami synchronizacyjnymi, byc moze bardziej adekwatnymi do zadania.

kwietnia 12, 2011 14:06

Co o tym myślisz?

(wymagane) 
(opcjonalne)
(wymagane) 

  
Wprowadź kod: (wymagane)
Wyślij

Subskrypcje

W oparciu o Community Server (Personal Edition), Telligent Systems