Archive for the ‘Events’ Category

Forbedret håndtering af events i C# 4

Tuesday, March 23rd, 2010

Dette indlæg bygger på Chris Burrows’ nylige indlæg (her og her) om, hvordan håndtering af events er blevet ændret i den kommende version af C#-compileren. Læs endelig begge indlæg! Og læs dem så lige igen. Savner du yderligere forklaring omkring problemet, kan dette indlæg forhåbentlig besvare de resterende spørgsmål.

Problemerne

Der er to problemer med den nuværende compilers måde at generere kode for håndtering af events. Det ene problem er uheldigt, og kan desværre lede til deadlocks i koden. Heldigvis er dette problem forholdsvis ligetil at forstå.

Det andet problem er derimod mere subtilt, men humlen er, at compileren i visse situationer vil generere kode, der ikke er thread safe. Konsekvenserne af dette kan udarte sig på mange forskellige måder, og det kan derfor være svært, at finde sammenhængen.

Lad os se på det første problem.

Som jeg tidligere har været inde på, behandler compileren events specielt. Så når vi erklærer en event som følger

public event EventHandler SomeEvent;

genererer compileren et privat felt af typen EventHandler samt metoderne add_SomeEvent og remove_SomeEvent. Implementeringen af disse metoder er dekoreret med MethodImplOptions.Synchronized, hvilket vil sige, at de kalder lock(this) for at sikre udelt adgang til den underliggende delegate-liste. Det er en dårlig ide, at bruge en offentlig tilgængelig reference til låsning. Da this i sagens natur ikke er privat for instansen, kan dette føre til deadlocks.

Det er der ikke så meget at gøre ved på nuværende tidspunkt. Følger man anbefalingen om at altid låse via en dedikeret, privat instans af object er risikoen for problemer i denne sammenhæng begrænset.

Heldigvis er kodegenereringen ændret i den kommende version af compileren, så ikke alene er deadlock-problematikken håndteret, men da implementeringen nu undgår låse, kan vi også regne med en lille hastighedsforbedring. add_SomeEvent ser nu ud som følger:

public void add_SomeEvent(EventHandler value)
{
   EventHandler handler2;
   EventHandler someEvent = this.SomeEvent;
   do
   {
      handler2 = someEvent;
      EventHandler handler3 = (EventHandler) Delegate.Combine(handler2, value);
      someEvent = Interlocked.CompareExchange<EventHandler>(ref this.SomeEvent, handler3, handler2);
   }
   while (someEvent != handler2);
}

Det andet problem

Det næste problem er langt mere underfundigt. Betragt nedenstående type:

public sealed class SomeType {
    public event EventHandler SomeEvent;
    private int Count = 0;

    public void AddInternalEvent(object o) {
       SomeEvent += SomeEventHandler;
       Interlocked.Increment(ref Count);
    }

    private void SomeEventHandler(object sender, EventArgs args) { }

    public void ReportNumberOfListeners() {
       Console.WriteLine("Listeners {0}", SomeEvent.GetInvocationList().Length);
       Console.WriteLine("AddInternalEvent called {0} times", Count);
    }
}

Typen er ikke videre interessant, men den implementerer en event, der kan modificeres via AddInternalEvent(). Læg også mærke til, at den holder styr på, hvor mange gange denne metode er blevet kaldt for hver instans. Det er egentlig ikke nødvendig, men det fjerner eventuel usikkerhed omkring timing af de enkelte tråde.

Hvad sker der, hvis vi kalder AddInternalEvent() fra mange samtidige tråde? Her er et eksempel, hvor vi sætter 100 kald over via .NETs thread pool:

var st = new SomeType();

for (int i = 0; i < 100; i++) {
   ThreadPool.QueueUserWorkItem(st.AddInternalEvent);
}

Thread.Sleep(1000);

st.ReportNumberOfListeners();

Formålet med denne kode er blot at illustrere problemet. Jeg er klar over, at man ikke ville skrive kode som ovenfor, men hvis mere end en tråd kalder AddInternalEvent samtidigt, kan problemet opstå. Ved at kalde den mange gange som her, øger vi sandsynligheden for, at vi vil se effekten af problemet.

Ovenstående skulle gerne udskrive, at metoden er blevet kaldt 100 gange, samt at vi derfor har 100 listeners. Desværre er det ikke altid tilfældet. Vores tæller viser, at metoden bliver kaldt 100 gange som forventet, men alligevel har vi ikke altid 100 listeners.

Vi så jo lige, at add_SomeEvent() sørger for at kalde lock(this), hvilket godt nok kan føre til deadlocks, men trods alt burde synkronisere adgangen til event-listen. Hvordan kan det så gå galt?

Problemet er, at add_SomeEvent() ikke bliver kaldt i dette tilfælde!

Hvis en type specificerer en event og selv manipulerer denne via +=, genererer compileren ikke et kald til add_SomeEvent(), men går i stedet direkte på det underliggende delegate field og kalder Combine() på dette. (Det tilsvarende gør sig naturligvis gældende for -=). Her er vores kode til AddInternalEvent(), som den ser ud i .NET Reflector:

public void AddInternalEvent(object o)
{
   this.SomeEvent = (EventHandler) Delegate.Combine(this.SomeEvent, new EventHandler(this.SomeEventHandler));
   Interlocked.Increment(ref this.Count);
}

Som det fremgår af ovenstående, kaldes Delegate.Combine() direkte, og der er således ingen synkronisering. Derfor er koden i dette tilfælde ikke thread safe. Jeg er overbevist om, at Chris Burrows har ret, når han skriver, at størstedelen af C#-udviklere næppe er opmærksomme på denne subtile, men vigtige forskel.

Som nævnt indtræffer dette kun under visse omstændigheder. Såvel event-typen som brugen af den skal være defineret i samme type. Sagt på en anden måde: Hvis SomeEvent kaldes udefra, eller hvis eventen er defineret på en baseklasse (hvilket er tilfældet for mange GUI-komponenter), genererer compileren kald til add_SomeEvent() som forventet.

Heldigvis er dette problem også adresseret i den kommende version af compileren. Compileren genererer nu også kald til add_SomeEvent i dette tilfælde, hvilket vi kan verificere ved at se på den genererede IL.

.method public hidebysig instance void AddInternalEvent(object o) cil managed
{
   .maxstack 8
   L_0000: ldarg.0
   L_0001: ldarg.0
   L_0002: ldftn instance void TestApp.SomeType::SomeEventHandler(object, class [mscorlib]System.EventArgs)
   L_0008: newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
   L_000d: call instance void TestApp.SomeType::add_SomeEvent(class [mscorlib]System.EventHandler)
   L_0012: ldarg.0
   L_0013: ldflda int32 TestApp.SomeType::Count
   L_0018: call int32 [mscorlib]System.Threading.Interlocked::Increment(int32&amp;amp;amp;amp;amp;)
   L_001d: pop
   L_001e: ret
}

De ufødte GUI-komponenter, der nægter at dø!

Friday, April 27th, 2007

Hvis en type implementerer IDisposable, så er det et signal til brugeren, om at instanser af denne type anvender en eller flere begrænsede ressourcer, som brugeren bør frigive, så snart de ikke længere er nødvendige, ved at kalde Dispose(). Dialoger, skærme og andre GUI-elementer er alle Components og Component implementerer IDisposable. Ergo, GUI-kontroller implementerer Dispose(). Så hvis vi skal følge spillereglerne, skal vi huske at kalde Dispose(), hver gang vi opretter objekter af disse typer. Gør vi det, burde alting være i orden, og så ville der nok ikke være kommet et indlæg ud af det. Situationen er imidlertid den, at alting ikke er i orden.

Nedenstående eksempel er lettere fortænkt, idet man næppe ville anvende Controls på den måde, men det burde ikke desto mindre fungere. Vi opretter instanser af System.Windows.Forms.Control, kalder Dispose() (via using) og referencerne går tilmed ud af scope, så der burde ikke være spor af vores Controls, hvis vi inspicerer heapen.

class Program {
   static void Main(string[] args) {
      Runner r = new Runner();
      r.Run();

      System.GC.Collect();
      Console.ReadLine();
   }
}

public class Runner {
   public void Run() {
      for (int i = 0; i < 5; i++) {
         using (System.Windows.Forms.Control c =
                  new System.Windows.Forms.Control()) { }
      }
   }
}

Via et hang dump, kan vi undersøge hvordan heapen ser ud på tidspunktet for Console.ReadLine().

0:000> !dumpheap -stat -type Form
total 41 objects
Statistics:
      MT    Count    TotalSize Class Name
7b46ec80        1           20 System.Windows.Forms.WindowsFormsSynchronizationContext
7b46d1e0        1           28 System.Windows.Forms.Padding
7b49c1bc        1           36 System.Windows.Forms.AnchorStyles[]
7b46fae0        1           44 System.Windows.Forms.NativeWindow+WindowClass
7b46e894        1           52 System.Windows.Forms.CreateParams
7b46fd84        2           64 System.Windows.Forms.NativeMethods+WndProc
7b46e71c        6           96 System.Windows.Forms.PropertyStore+SizeWrapper
7b46d674        6           96 System.Windows.Forms.PropertyStore
7b46f294        1          104 System.Windows.Forms.Application+MarshalingControl
7b46eef4        1          132 System.Windows.Forms.Application+ThreadContext
7b49c090        6          192 System.Windows.Forms.PropertyStore+ObjectEntry[]
790ffe6c        2          256 System.Globalization.NumberFormatInfo
7b46d738        6          336 System.Windows.Forms.Control+ControlNativeWindow
7b49c2e8        1          456 System.Windows.Forms.NativeWindow+HandleBucket[]
7b46b3fc        5          520 System.Windows.Forms.Control
Total 41 objects

Hvad laver vores fem Controls der?! Der er da ingen, der holder fast i dem længere, så hvorfor har garbage collectoren ikke taget dem? Der må være noget, vi har overset, så vi sætter WinDbg til at fortælle os, hvem der holder de fem Controls i live.

0:000> .foreach (mt { !dumpheap -short -mt 7b46b3fc }) { !gcroot mt }
Note: Roots found on stacks may be false positives.
Run "!help gcroot" for more info.
Scan Thread 0 OSTHread 1a0
Scan Thread 2 OSTHread 87c
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 1a0
Scan Thread 2 OSTHread 87c
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 1a0
Scan Thread 2 OSTHread 87c
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 1a0
Scan Thread 2 OSTHread 87c
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 1a0
Scan Thread 2 OSTHread 87c

Det gav ikke meget. Ifølge WinDbg er der ingen referencer til vores instanser, så de burde være blevet taget af garbage collectoren (vi tvinger en oprydning i gang med et kald til Collect()).

Jeg må indrømme, at jeg desværre ikke kan give et fyldestgørende svar på præcis hvad, der holder disse Controls i live, men hvis man kalder CreateControl() på dem inden Dispose(), er alt i den skønneste orden. Så hvis vi modificerer Run() som vist nedenfor, bliver der ryddet pænt op.

public void Run() {
   for (int i = 0; i < 5; i++) {
      using (System.Windows.Forms.Control c =
               new System.Windows.Forms.Control()) {
                  c.CreateControl();
      }
   }
}

Efter kald til CreateControl() er der ikke længere zombie-Controls på heapen:

0:000> !dumpheap -stat -type Form
total 25 objects
Statistics:
      MT    Count    TotalSize Class Name
7b46ec80        1           20 System.Windows.Forms.WindowsFormsSynchronizationContext
7b46d1e0        1           28 System.Windows.Forms.Padding
7b46e71c        2           32 System.Windows.Forms.PropertyStore+SizeWrapper
7b46d674        2           32 System.Windows.Forms.PropertyStore
7b49c1bc        1           36 System.Windows.Forms.AnchorStyles[]
7b46ea54        1           40 System.Windows.Forms.VScrollProperties
7b46e998        1           40 System.Windows.Forms.HScrollProperties
7b46fae0        1           44 System.Windows.Forms.NativeWindow+WindowClass
7b49c090        2           64 System.Windows.Forms.PropertyStore+ObjectEntry[]
7b46fd84        3           96 System.Windows.Forms.NativeMethods+WndProc
7b46f294        1          104 System.Windows.Forms.Application+MarshalingControl
7b46e894        2          104 System.Windows.Forms.CreateParams
7b46d738        2          112 System.Windows.Forms.Control+ControlNativeWindow
7b46eef4        1          132 System.Windows.Forms.Application+ThreadContext
7b48531c        1          220 System.Windows.Forms.Application+ParkingWindow
790ffe6c        2          256 System.Globalization.NumberFormatInfo
7b49c2e8        1          456 System.Windows.Forms.NativeWindow+HandleBucket[]
Total 25 objects

Det underlige i denne sammenhæng er, at grunden til, at CreateControl() eksisterer, er fordi, vi ønsker at udskyde tilknytningen af et Handle (HWND) til Controllen så længe som mulig, da det er en krævende operation, der samtidig lægger beslag på en begrænset ressource. Derfor er det ret underligt, at dette faktisk er nødvendigt for at sikre, at der bliver ryddet ordentlig op.

Problemet er som nævnt en anelse fortænkt, eftersom man næppe vil bruge Controls uden at kalde CreateControl(), men det ændrer ikke ved, at Microsoft med denne konstruktion har introduceret en model, der ikke opfylder de basale spilleregler for IDisposable, og eftersom WinDbg ikke kan se nogle roots til vores objekter, er det nærliggende at tro, at CLRen på anden vis kan holde objekter i live, hvilket ærlig talt ikke er særlig betryggende. Hvis nogen har en uddybende forklaring på, hvorfor CreateControl() gør forskellen i denne sag, er jeg meget interesseret i at høre om det.

Find memoryproblemer med WinDbg

Thursday, April 26th, 2007

I mit forrige indlæg beskrev jeg, hvordan jeg har brugt WinDbg til at finde diverse memoryproblemer i managed code, så til dem, der ikke kender WinDbg, følger her en kort introduktion.

WinDbg er en user/kernel mode debugger, der kommer som en del af Microsofts Debugging Tools for Windows. WinDbg er som udgangspunkt rettet mod unmanaged code, men via Son of Strike-udvidelsen (sos.dll) bliver den ret interessant for C#-udviklere.

For at få flere detaljer i WinDbg er det en god ide at hente Microsofts symbolpakke og sætte _NT_SYMBOL_PATH til at pege på disse samt symboler fra den .NET SDK (ligger som standard under Microsoft Visual Studio 8\SDK\v2.0\symbols). Det er også en god ide at slette clr10-biblioket under Debugging Tools for Windows, hvis man ikke har brug for support af CLR version 1.0. På den måde slipper man for at fortælle WinDbg hvilken version af sos.dll, man skal bruge.

Jeg har lavet et lille eksempel, der illustrerer et memoryproblem, der minder meget om det, vi konstaterede i det GUI-bibliotek, vi anvender.

namespace ConsoleApplication {
   class Program {
      static void Main(string[] args) {
         for (int i = 0; i < 10; i++) {
            FancyLayout st = new FancyLayout();
         }
         System.GC.Collect();
         Console.ReadLine();
      }
   }

   public class FancyLayout : IDisposable {
      public FancyLayout() {
         SkinManager.Default.SkinChanged += OnSkinChanged;
      }

      public void OnSkinChanged(object src, EventArgs args) {
      }

      public void Dispose() {
         SkinManager.Default.SkinChanged -= OnSkinChanged;
      }
   }

   public class SkinManager {
      public event EventHandler SkinChanged;

      public static SkinManager Default {
         get {
            if (vDefault == null) {
               vDefault = new SkinManager();
            }
            return vDefault;
         }
      }
      private static SkinManager vDefault;

      public void ChangeSkin() {
         if (SkinChanged != null) {
            SkinChanged(this, EventArgs.Empty);
         }
      }
   }
}

Ovenstående er forsimplet en del, men sagens kerne er den samme. Vi har et antal skins – her eksemplificeret af FancyLayout – og en singleton, der sørger for at det valgte skin underrettes, når det skal. Det er ikke overraskende lavet via events, så et givet layout kobler sig på manageren for at blive orienteret i tilfældet af ændringer. Da events medfører en hård reference fra publisher til subscriber, skal subscriberen altid huske at framelde notifikationer som en del af sin oprydning (det var lektien i mit forrige indlæg). Gøres dette ikke, holdes de i live af publisheren. Da publisheren i dette tilfælde er static, betyder det i praksis, at alle vores layouts bliver hængende så længe applikationen kører! Det kan vi bruge WinDbg til at konstatere.

For at undersøge memoryproblemer har vi brug for et såkaldt hang dump. Et hang dump er et komplet dump af en kørende proces. Den kørende proces identificeres enten via procesid eller navn. Bruger man navnet er det en god ide at slå Visual Studios hosting process fra i Debug-sektionen under projektets Properties.

I og med at der er tale om live memory, er det ikke uvæsentlig, hvornår dumpet bliver taget. I vores lille eksempel har jeg sat en Console.ReadLine() ind, så vi let kan tage dumpet på et fornuftigt tidspunkt. Så heldig er man ikke altid med rigtige applikationer, så derfor skal man være meget opmærksom på hvornår man tager dumpet. I alle tilfælde kan vi bruge adplus.vbs til at skaffe vores dump. Et hang dump tages således:

adplus –hang –pn <navn på process> -o <placering af dump>

eller

adplus –hang –p <pid> -o <placering af dump>

Jeg tog et hang dump af ovenstående med følgende kommando:

adplus -hang -o c:temp -pn consoleapplication.exe

Cirka 30 sekunder senere ligger der et hang dump under temp, og så er det tid til WinDbg.

WinDbg skelner ikke mellem crash og hang dumps, så bare vælg Hang dump fra File-menuen for at åbne det relevante dump. Herefter skal sos.dll indlæses:

.load sos

(bemærk punktum foran load)

Herefter er vi klar til at undersøge vores lille applikation. Der er hjælp til brug af sos.dll via kommandoen !help og til de enkelte kommandoer via !help <kommando>.

Det første, vi er interesserede i, er, hvordan den managed heap ser ud:

0:000> !dumpheap -stat
total 2218 objects
Statistics:
MT    Count    TotalSize Class Name
790fdd5c        1           12 System.Security.Permissions.SecurityPermission
00963164        1           12 ConsoleApplication.SkinManager
791783bc        1           16 System.IO.TextReader+SyncTextReader
7910031c        1           20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
791002c0        1           20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
790ff26c        1           20 System.Text.InternalEncoderBestFitFallback
790fdf38        1           20 Microsoft.Win32.SafeHandles.SafeFileHandle
790ff2c4        1           24 System.Text.InternalDecoderBestFitFallback
790fd5b4        1           24 System.OperatingSystem
790fd4ec        1           24 System.Version
790fc79c        1           24 System.Reflection.Assembly
79115d98        1           28 System.Text.DecoderNLS
790fe4b0        1           28 System.IO.Stream+NullStream
790fde94        1           28 Microsoft.Win32.Win32Native+InputRecord
790fb668        1           28 System.SharedStatics
79124544        1           36 System.Int64[]
790fe280        1           36 System.IO.__ConsoleStream
790fbcfc        1           40 System.AppDomainSetup
790ffbe4        4           48 System.UInt16
79118ef0        1           60 System.IO.StreamReader
790ffa34        1           72 System.Globalization.CultureTable
790fd824        2           72 System.Security.PermissionSet
790fac70        1           72 System.ExecutionEngineException
790fabcc        1           72 System.StackOverflowException
790fab28        1           72 System.OutOfMemoryException
790ff138        1           76 System.Text.SBCSCodePageEncoding
791240f0        5           80 System.Int32[]
790f9c18        7           84 System.Object
790ffb28        5          100 System.Globalization.CultureTableItem
790fb8c8        1          100 System.AppDomain
790fa800        5          100 System.Text.StringBuilder
790fed1c        9          108 System.Int32
00963090       10          120 ConsoleApplication.FancyLayout
790ffe6c        1          128 System.Globalization.NumberFormatInfo
790fca24        2          128 System.IO.UnmanagedMemoryStream
790ff6dc        3          144 System.Globalization.CultureTableRecord
790fad14        2          144 System.Threading.ThreadAbortException
790fc308        8          160 System.RuntimeType
790ff4c4        3          216 System.Globalization.CultureInfo
79124418        2          280 System.Byte[]
790fea70        5          280 System.Collections.Hashtable
7910d61c       11          352 System.EventHandler
791242ec        5          720 System.Collections.Hashtable+bucket[]
79124670        6          928 System.Char[]
00188fb8       23         1780      Free
79124228       11         9092 System.Object[]
790fa3e0     2064       130160 System.String
Total 2218 objects

Det kan give en hel del output, så det kan være en god ide at bruge -type, der begrænser rapporten til linjer, der indeholder den angivne tekst:

0:000> !dumpheap -stat -type ConsoleApplication
total 11 objects
Statistics:
MT    Count    TotalSize Class Name
00963164        1           12 ConsoleApplication.SkinManager
00963090       10          120 ConsoleApplication.FancyLayout
Total 11 objects

Som forventet (da vi jo ikke rydder pænt op) ligger der 10 instanser af FancyLayout skønt vores applikation ikke længere anvender de 9 af dem på tidspunktet for dumpet.

WinDbg kan fortælle os hvem, der holder disse i live. For at gøre det skal vi have fat i adressen på de konkrete FancyLayout-objekter. Ovenstående udskrift lister en MethodTable (MT) pointer for hver type. Via denne kan vi finde adressen på alle instanser af en give type.

0:000> !dumpheap -mt 00963090
Address       MT     Size
01351b68 00963090       12
01351ba0 00963090       12
01351c04 00963090       12
01351c70 00963090       12
01351cbc 00963090       12
01351d38 00963090       12
01351d84 00963090       12
01351dd0 00963090       12
01351e1c 00963090       12
01351eb8 00963090       12
total 10 objects
Statistics:
MT    Count    TotalSize Class Name
00963090       10          120 ConsoleApplication.FancyLayout
Total 10 objects

Vi kan undersøge de enkelte instanser med !dumpobj <address> og få deres reelle størrelse med !objsize <address>, men det er ikke nødvendig i dette tilfælde. I stedet vælger vi en tilfældig instans fra listen og bruger !gcroot til at finde ud af hvem, der holder denne i live:

0:000> !gcroot 01351d84
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread e1c
Scan Thread 2 OSTHread e2c
DOMAIN(0014BF08):HANDLE(Pinned):9413fc:Root:02351010(System.Object[])->
01351b74(ConsoleApplication.SkinManager)->
01351ee4(System.EventHandler)->
01351e48(System.Object[])->
01351d90(System.EventHandler)->
01351d84(ConsoleApplication.FancyLayout)

Ifølge ovenstående hænger dette FancyLayout-objekt fast via en EventHandlerSkinManager. Vi kan tilmed se at SkinManager er holdt fast af en Pinned Object-array.

Med denne viden kan vi gå tilbage og rette fejlen i vores eksempel. Hvis vi ændrer Main som følger forsvinder problemet:

static void Main(string[] args) {
   for (int i = 0; i < 10; i++) {
      using (FancyLayout st = new FancyLayout()) { }
   }
   System.GC.Collect();
   Console.ReadLine();
}

hvilket vi kan konstatere med et nyt dump:

0:000> !dumpheap -stat -type ConsoleApplication
total 2 objects
Statistics:
MT    Count    TotalSize Class Name
00963164        1           12 ConsoleApplication.SkinManager
00963090        1           12 ConsoleApplication.FancyLayout
Total 2 objects

Det sidste par instanser holdes i live som følge af CLRens måde at implementere using-konstruktionen, hvilket jeg vil komme tilbage til i et senere indlæg. Det kan virke lidt underligt, eftersom der jo ikke er referencer til nogle instanser i ovenstående, men alt er som det skal være.

Som ovenstående illustrerer kan WinDbg + sos.dll være en stor hjælp til fejlsøgning. Der er flere relevante scenarier, som jeg vil vende tilbage til i fremtidige indlæg, men jeg vil lige hæfte en kommentar til kommandoen !finalizequeue, idet den førte til et par misforståelser hos mine kolleger og jeg.

!finalizequeue lister detaljer om garbage collectoren og den såkaldte finalize queue. Kører man kommandoen på ovenstående dump får vi følgende:

0:000> !finalizequeue
SyncBlocks to be cleaned up: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 3 finalizable objects (00192338->00192344)
generation 1 has 0 finalizable objects (00192338->00192338)
generation 2 has 0 finalizable objects (00192338->00192338)
Ready for finalization 0 objects (00192344->00192344)
Statistics:
MT    Count    TotalSize Class Name
7910031c        1           20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
791002c0        1           20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
790fdf38        1           20 Microsoft.Win32.SafeHandles.SafeFileHandle
Total 3 objects0:000> !finalizequeue
SyncBlocks to be cleaned up: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 3 finalizable objects (00192338->00192344)
generation 1 has 0 finalizable objects (00192338->00192338)
generation 2 has 0 finalizable objects (00192338->00192338)
Ready for finalization 0 objects (00192344->00192344)
Statistics:
MT    Count    TotalSize Class Name
7910031c        1           20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
791002c0        1           20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
790fdf38        1           20 Microsoft.Win32.SafeHandles.SafeFileHandle
Total 3 objects

Vi kan se status for de enkelte generationer samt en oversigt over typer i finalize-køen. Forvirringen kommer af at finalize-køen ikke indeholder objekter, der venter på at få deres finalize-metode kørt. Den kø hedder freachable (udtales f-reachable). Finalize-køen indeholder alle typer, der har en finalize-metode. Når garbage collectoren skal til at nedlægge et objekt, undersøger den om typen er på finalize-køen og i så fald opretter den en reference til objektet på freachable. Ergo, man kan altså ikke bruge !finalizequeue til at finde objekter, der venter på at få kaldt deres finalize-metode.

Sejlivede events giver problemer

Monday, April 23rd, 2007

Jeg har brugt nogle dage på at stirre mig blind på dumps i WinDbg for at finde årsagen til en række memoryproblemer, vi først konstaterede i vores testmiljø. Det har været en interessant oplevelse, der har afsløret et par alarmerede detaljer omkring C#. Det er for omfattende, at gennemgå dem alle i detaljer, så her er den korte version af historien.

Vores oprindelige problem var, at vores testmetoder leakede og med knap 2000 testmetoder, gav det problemer. Det første, vi konstaterede, var at statics er meget problematiske i testmetoder, eftersom alle testmetoder afvikles i samme AppDomain, og dermed bliver det samlede memoryforbrug lig foreningsmængden af disse statics. Endnu et eksempel på at statics er evil, så undgå dem i testmetoder (og faktisk i al almindelighed).

Den næste observation var, at System.EventHandler tilsyneladende holdt objekter i live. Det havde vi svært ved at tro, for vi var sikre på, at events var implementerede via en svag reference. Det er de bare ikke (!), så da alle andre muligheder var udtømt, vendte vi – nærmest i trods – tilbage til denne observation. Jeg lavede et lille program, tog nogle hang-dumps og satte WinDbg på arbejde.

class Program {
   static void Main(string[] args) {
      Publisher publisher = new Publisher();
      Subscriber subscriber = new Subscriber(publisher);
      publisher.DoSomethingAndNotify();
      subscriber = null;
      System.GC.Collect();

      Console.ReadLine();
   }

   public class Subscriber {
      public Subscriber(Publisher p) {
         p.SomeEvent += ReactToEvent;
      }

      void ReactToEvent(object sender, EventArgs e) {
         Console.WriteLine("react to event ...");
      }
   }

   public class Publisher {
      public event EventHandler SomeEvent;

      public void DoSomethingAndNotify() {
         if (SomeEvent != null) {
            SomeEvent(this, EventArgs.Empty);
         }
      }
   }
}

Ovenstående laver en instans af Publisher og Subscriber. Som navnene antyder abonnerer Subscriber på orienteringer om ændringer i Publisher, så når Publisher laver noget spændende (i form af DoSomethingAndNotify), får Subscriber besked og meddeler dette til omverdenen ved at skrive en besked på konsollen. Derefter nedlægger vi referencen til Subscriber-instansen, og således skulle man tro, at denne instans kan ryddes af vejen af garbage collectoren. Det sker bare ikke, for som nedenstående WinDbg-dump viser, er både Subscriber og Publisher i live på tidspunktet for Console.ReadLine().

0:000> !dumpheap -stat -type Console
total 4 objects
Statistics:
MT    Count    TotalSize Class Name
00963160        1           12 ConsoleApplication.Program+Subscriber
009630c4        1           12 ConsoleApplication.Program+Publisher
01242a0c        2           72 System.IO.__ConsoleStream
Total 4 objects

Det eksplicitte kald til GC.Collect() sikrer, at vi har forsøgt at oprydde, hvad der kan ryddes op, så det er ikke bare et spørgsmål om uheldig timing. Der er en instans af Subscriber i live. Den formodede løse kobling mellem en Publisher og en Subscriber er temmelig fast, når det kommer til stykket.

Det næste spørgsmål er selvfølgelig: Hvem holder Subscriber-instansen i live? Det kan WinDbg også hjælpe os med.

0:000> !dumpheap -mt 00963160
Address       MT     Size
0135254c 00963160       12
total 1 objects
Statistics:
MT    Count    TotalSize Class Name
00963160        1           12 ConsoleApplication.Program+Subscriber
Total 1 objects
0:000> !gcroot 0135254c
Note: Roots found on stacks may be false positives.
Run "!help gcroot" for more info.
Scan Thread 0 OSTHread 1020
ESP:12f458:Root:01352540(ConsoleApplication.Program+Publisher)->
01352558(System.EventHandler)->
0135254c(ConsoleApplication.Program+Subscriber)
ESP:12f464:Root:01352540(ConsoleApplication.Program+Publisher)->
01352558(System.EventHandler)
ESP:12f46c:Root:0135254c(ConsoleApplication.Program+Subscriber)->
01352558(System.EventHandler)
Scan Thread 2 OSTHread 84c

Ovenstående viser, at Subscriber-instansen holdes i live af en Publisher, der via en event har en reference til vores Subscriber. Ergo, C# benytter ikke som man kunne have håbet en svag reference i dette tilfælde. Det kom som en stor overraskelse for mine kolleger og jeg.

Løsningen på problemet er naturligvis at Subscribers skal sørge for at opsige deres abonemment(er), inden de lægger sig til at dø, men det er faktisk lettere sagt end gjort.

En nærliggende måde at gøre dette er ved at lade Subscriber implementere IDisposable og så sørge for at rydde pænt op i Dispose():

public class Subscriber : IDisposable {
   private Publisher publisher;
   public Subscriber(Publisher p) {
      publisher = p;
      publisher.SomeEvent += ReactToEvent;
   }

   void ReactToEvent(object sender, EventArgs e) {
      Console.WriteLine("react to event ...");
   }

   public void Dispose() {
      publisher.SomeEvent -= ReactToEvent;
   }
}

Læg mærke til at det ikke kan gøres i en finalize-metode, for i og med at Publisher holder en hård reference til Subscriber, vil Subscriber-instansen ikke blive ryddet af vejen af garbage collectoren og dermed bliver dens finalize-metode ikke kaldt tids nok til at hjælpe os i denne situation. Ergo, skal oprydningen lægges i Dispose(), men i modsætning til finalize-metoder bliver Dispose() jo ikke kaldt automatisk. Derfor skal brugeren sørge for at kalde Dispose(). Der er ingen grund til at tro, at det er væsentlig lettere at huske at kalde Dispose() end at huske at framelde events, så IDisposable giver os blot et sted at samle oprydningen. Glemmer brugeren at kalde Dispose(), er vi ikke bedre stillet end før.

En mere komplet løsning er at lave en ny implementering af event-mekanismen, der anvender svage referencer (Microsoft har en lettere vingeskudt WeakReference-type, der kan bruges til formålet). Det er der mange, der har forsøgt, men kun få har haft held til at lave noget brugbart (her er et godt bud på en løsning). Desværre medfører selv de brugbare løsninger et par udfordringer. For det første kan de ikke løse problemet i kode, vi ikke har kontrol over. For det andet introducerer det valgte alternativ en anderledes måde at gøre tingene på, og derfor skal udviklerne ”omskoles” i denne nye måde, og vi skal frem for alt sikre os, at der ikke er nogen, der bruger den problematiske konstruktion. Der er bare en, der skal glemme at rydde op, før vi har miseren. Det er alt andet lige ikke særlig elegant.

Der er ikke noget, der tyder på, at Microsoft løser dette problem indenfor den nærmeste fremtid, men hvis de gør, vil den løsning være at foretrække, eftersom det vil være standardmåden at gøre det på. Indtil da må man enten selv sørge for at rydde op eller se nærmere på ovennævnte løsning.

Desværre er konklusionen på hele denne oplevelse, at de store løfter om, at C# befrier os for at tænke på allokering og oprydning, kun er en halv sandhed. Dette er desværre endnu et eksempel på, at man som C#-udvikler skal have tjek på spidsfindige implementeringsdetaljer for at undgå alvorlige problemer.

Events og delegates i C#

Thursday, March 8th, 2007

Et par kolleger og jeg kom til at tale om de eksakte forskelle på at erklære en event og en delegate i en klasse, og skønt det ikke er synderligt kompliceret, er der alligevel en del detaljer, så her følger en kort gennemgang.

I begge tilfælde er en delegate-type nødvendig. En sådan kan f.eks. erklæres som følger:

public delegate void DelegateType();

Herefter kan events og delegates erklæres:

public event DelegateType TheEvent;
public DelegateType TheDelegate;

Ved første øjekast kan TheEvent og TheDelegate anvendes på samme måde. Det er således muligt at sætte en listener-metode op via nøjagtig den samme syntaks:

TheEvent += Listener;
TheDelegate += Listener;

(Listener skal naturligvis være en metode af typen DelegateType i dette tilfælde).

De kan naturligvis også fjernes igen med:

TheEvent -= Listener;
TheDelegate -= Listener;

Både events og delegates understøtter altså muligheden for at have flere listeners tilknyttet. Det skyldes, at de begge implementeres via MulticastDelegate, der implementerer denne funktionalitet. Forskellen mellem events og delegates skal altså findes andetsteds. Hvis vi tager et kig på den genererede IL-kode, finder vi forklaringen.

.field public class TheApp.TheClass/DelegateType TheDelegate
.field private class TheApp.TheClass/DelegateType TheEvent

De ser forbløffende ens ud, men læg mærke til at TheEvent er private skønt erklæringen ovenfor er public. I forbindelse med events opretter compileren desuden to yderligere metoder.

.method public hidebysig specialname instance void
add_TheEvent(class TheApp.TheClass/DelegateType 'value')
cil managed synchronized
.method public hidebysig specialname instance void
remove_TheEvent(class TheApp.TheClass/DelegateType 'value')
cil managed synchronized

Implementeringen af disse er blot kald til MulticateDelegates funktioner til tilføjelse og fjernelse af delegates fra multicast-listen, så hvad betyder disse forskelle i praksis?

I og med at compileren laver implementeringen af events til private, uanset at de bliver erklæret public, betyder det, at brugere af den omsluttende klasse ikke har direkte mulighed for at manipulere variable erklæret som event. Den eneste måde at påvirke TheEvent er således gennem + og operatorerne, og dette lille stunt sikrer faktisk, at brugere af klassen ikke kommer til at slette en liste af listeners. Det er således muligt at skrive:

TheClass c = new TheClass();
c.TheDelegate = null; // nulstil kæden af listeners!

men umuligt at skrive:

TheClass c = new TheClass();
c.TheEvent = null; // oversætter ikke

Ovenstående giver en oversætterfejl. Så skønt events og delegates begge understøtter den basale listener-funktionalitet, giver events bedre beskyttelse mod brugerfejl, der kan være svære at fejlsøge.