Archive for the ‘Asynkron’ Category

Introduktion til AppDomains – anden del

Tuesday, May 6th, 2008

I tidligere indlæg så vi på motivationen for at bruge AppDomains i en applikation. I dette indlæg bliver det lidt mere konkret, da vi skal se på de nødvendige konstruktioner i forbindelse med brug af AppDomains.

AppDomains er repræsenteret ved klassen AppDomain, og oprettelse sker via den statiske metode CreateDomain(). Derefter skal vi have et eller flere assemblies indlæst, hvilket kan gøres på forskellige måder afhængig af, hvordan vi ønsker at afvikle kode. Når vi er færdige, kan vi nedlægge vores AppDomain og dermed frigive de indlæste assemblies. Sidstnævnte gøres via den statiske metode Unload()AppDomain.

Hvis vi lægger ud med den simpleste situation, hvor vi blot ønsker at lave et AppDomain, indlæse en applikation i dette og efterfølgende nedlægge AppDomainet, ser koden ud som følger:

AppDomain domain = AppDomain.CreateDomain("New AppDomain");
domain.ExecuteAssembly("application.exe");
AppDomain.Unload(domain);

Det er simpelt, men i realiteten heller ikke så interessant. I dette tilfælde opretter vi et ny AppDomain og indlæser en applikation, der derefter afvikles. Det vil sige, at de assemblies, application.exe afhænger af, indlæses i lokalt i domain. I og med at der er tale om en applikation, vil afviklingen af application.exe betyde, at Main() bliver kaldt ganske som forventet. Når denne er kørt til ende, returnerer kontrollen til vores applikation, hvor vi nedlægger domain og slipper dermed af med de assemblies, der er blevet indlæst i dette.

Ovenstående fungerer kun med applikationer, da de har et veldefineret entry point. Ønsker vi at kalde en vilkårlig statisk metode i et assembly, skal vi bruge DoCallBack()-metoden som vist her:

static void Main(string[] args) {
   AppDomain domain = AppDomain.CreateDomain("New AppDomain");
   domain.DoCallBack(Hello);
   AppDomain.Unload(domain);
}

static void Hello() {
   Console.WriteLine("Location: {0}", AppDomain.CurrentDomain.FriendlyName);
}

Læg mærke til, at vi via AppDomain.CurrentDomain kan aflæse det konkrete AppDomain. I eksemplet indlæser vi en kopi af den kaldende applikation, for derefter at kalde metoden Hello(). Det er selvfølgelig et noget konstrueret eksempel, der kun tjener til at illustrere syntaksen.

Afviklingen af koden i domain sker i begge tilfælde synkront. Vores applikation kan altså ikke lave andet, mens application.exe afvikler, og derfor er disse fremgangsmåder kun interessante i et begrænset antal tilfælde.

Som nævnt er der nemlig ikke sammenhæng mellem AppDomains og tråde. Vores kode afvikles i praksis ved, at den kaldende applikations tråd simpelthen hopper fra det ene AppDomain til det andet og tilbage igen.

Hvis vi vil have kode i vores AppDomains til at afvikle selvstændigt, skal vi således have flere tråde i spil, hvilket kan gøres på flere måder. Vi kan anvende tråde i CLRens thread pool, hvilket er en god ide til små opgaver. Til længere opgaver vil det nok være en bedre ide at oprette selvstændige tråde efter behov. I begge tilfælde kan vi lade det være op til den kaldende applikation eller til de enkelte delapplikationer at oprette/bruge de nødvendige tråde. Hvis vi overlader det til delapplikationerne, vil det være en god ide, at oprette en abstrakt baseklasse, der indkapsler genereringen af tråde. Det kunne f.eks. se ud som følger:

Vores interface specificerer to metoder: Start() og Stop(). Start() laver en tråd og sætter denne til at afvikle RunBusinessLogic(). Stop() signalerer til applikationen at den skal lukke ned. RunBusinessLogic() kalder den virtuelle metode BusinessLogic() og indkapsler håndtering af exceptions. Konkrete applikationer skal derfor blot arve fra Application og overstyre implementeringen af BusinessLogic().

En anden begrænsning ved den simple fremgangsmåde er, at vi ikke har nogen måde at påvirke afviklingen af koden i domain. En af ideerne er jo netop, at kode i et AppDomain ikke skal kunne påvirke kode i et andet, men hvis vi vil lave en værtsapplikation, der indlæser og afvikler forskellige delapplikationer dynamisk, har vi brug for at kunne kommunikere med de enkelte applikationer.

Kommunikation mellem forskellige AppDomains sker via remoting. Ved remoting marshalles typer over AppDomain-grænser via enten marshal by value eller marshal by reference. Førstnævnte kræver at typen er serializable og er kun interessant i et begrænset antal tilfælde, da dette medfører, at der oprettes en lokal kopi af den aktuelle type. Ved marshal by reference skal typen arve fra MarshalByRefObject. Herefter håndteres al marshalling automatisk. Det kaldende AppDomain får en reference til en konkret instans af den faktiske type i det andet domæne. Via denne kan vores værtsapplikation påvirke tilstanden af en instans i et andet domæne.

Ønsker vi at oprette en reference til en instans i et andet AppDomain, skal vi kalde CreateInstanceAndUnwrap() på det ønskede AppDomain som vist her:

AppDomain domain = AppDomain.CreateDomain("New AppDomain");
IApplication application = (IApplication)domain.CreateInstanceAndUnwrap("Applications", "Applications.WorkingApplication");
application.EntryPoint();

Læg mærke til, at vi anvender en interface-reference samt indlæser den ønskede type via en tekststreng og ikke via typens egen beskrivelse. Derved undgår vi, at værtsdomænet også indlæser de berørte assemblies. Det kræver naturligvis, at IApplication og de konkrete applikationer er erklæret i forskellige assemblies. Bemærk også, at det kan være nødvendig, at hjælpe AppDomain lidt på vej i forhold til placering af de relevante DLLer. Hvis der er behov for det, kan vi oprette en instans af AppDomainSetup, sætte de relevante stier og oprette vores AppDomain med den opsætning. Det har jeg dog udeladt i eksemplet af hensyn til overskueligheden.

I dette indlæg har vi set på, hvordan AppDomains kan oprettes samt hvordan vi kan kommunikere med dem. I næste indlæg skal vi se lidt på nogle af de tekniske detaljer i forhold til AppDomains.

Manglende EndInvoke() er måske ikke så galt endda

Wednesday, March 5th, 2008

“New shit has come to light!” som The Dude så smukt udtrykker det i The Big Lebowski, og derfor bliver jeg nødt til at kommentere et af mine tidligere indlæg. Det omhandlede den lidt specielle situation, at manglende kald til EndInvoke() på en delegate resulterer i et memory leak i selve CLRen.

Fænomenet er tilmed beskrevet i Jeffrey Richters glimrende bog CLR via C# 2nd edition (side 615), og på daværende tidspunkt specificerede Microsofts dokumentation, at BeginInvoke() altid skulle efterfølges af et passende kald til EndInvoke(). Denne notits er ikke længere en del af dokumentationen og på MSDNs .NET-forum bliver der sået dokumenteret tvivl om problemets størrelse.

Selv kom jeg på sporet af disse ændringer, eftersom jeg er ved at lave eksempler til min næste TechTalk. Jeg spildte et par timer på forgæves at eftervise det problem, jeg tidligere havde beskrevet. Jeg har ikke kunne finde en definitiv udmelding vedrørende problemet, men der er i hvert fald meget, der tyder på, at problemet ikke længere er så stort endda.

Jeg vil dog ikke gå så langt som at sige, at EndInvoke() er overflødig. Der er stadig gode grunde til at kalde EndInvoke() efter BeginInvoke().

  1. EndInvoke() giver adgang til et eventuelt resultat fra den asynkrone operation.
  2. EndInvoke() frigiver en eventuel exception fra den asynkrone operation, så den kan blive behandlet af den del af koden, der igangsatte kaldet.
  3. Selv hvis vi antager, at der virkelig ikke er noget leak længere, er det stadig en god ide at kalde EndInvoke() for på den måde at gøre oprydningen deterministisk.

PS: Og har man brug for “fire and forget”-funktionalitet, er ThreadPool.QueueUserWorkItem() et godt bud.

Når debuggeren afvikler vores kode

Monday, February 11th, 2008

En kollega hev fat i mig for nogle dage siden, for at vise mig en ret underlig opførsel i Visual Studio i forbindelse med debugging af en stump kode. Det, vi kunne observere, var en reference, der blev sat til null til trods for at det eneste kode, der kunne være skyld i dette tilsyneladende aldrig blev kørt. Situationen viste sig, at have en naturlig om end spidsfindig forklaring, men det så unægtelig meget underligt ud.

Jeg vil ikke gå i detaljer med min kollegas kode, men skåret til benet var problemet, at han ønskede at starte en proces via Process-klassen og blive underrettet, når processen blev lukket. I eksemplet stod han selv for at lukke processen. Nedenstående kodestump illustrerer situationen:

private static Process p;

static void Main() {
   Console.WriteLine("launching notepad");
   p = Process.Start("notepad.exe");

   p.Exited += new EventHandler(Exited);
   p.EnableRaisingEvents = true;

   Console.WriteLine("press enter to kill notepad.exe");
   Console.ReadLine();

   p.CloseMainWindow(); // breakpoint her
   p.Close();

   Console.WriteLine("process terminated");
   Console.ReadLine();
}

public static void Exited(object sender, EventArgs e) {
    Console.WriteLine("In Exited");  // og breakpoint her
    p = null;
}

Koden er ret simpel. Vi starter en ekstern proces via Process.Start(). Derefter kobler vi en callback metode på og anmoder om at blive underrettet, når processen lukkes. CloseMainWindow() sender en WM_CLOSE til vores instans af Notepad, hvilket giver den en chance for at lukke pænt ned og sørger samtidig for, at vores callback bliver kaldt. Af dokumentationen fremgår det, at vores callback vil blive kaldt asynkront, og hvis vi ser nærmere på Process i Reflector, kan vi se, at hele adviseringen er lavet via et WaitHandle i Threadpoolen. Close() rydder Process-instansen op og frakobler i den forbindelse også event-registreringen.

Jeg er klar over, at der mangler både oprydning og fejlhåndtering i ovenstående, men da det ikke påvirker resultatet i dette tilfælde, har jeg udeladt det af hensyn til overskueligheden.

Den spøjse situation bestod i, at vi havde sat et breakpoint på p.CloseMainWindow() og et i Exited() som angivet i kommentarerne i koden. Som forventet stoppede debuggeren på CloseMainWindow(). Et tryk på F10 bragte os videre til p.Close(), og i og med at Notepad hermed blev lukket, fik vi også sat hele notifikationsmøllen i sving.

Debuggeren stoppede dog ikke i Exited() på dette tidspunkt, hvilket kan forklares med, at vores event håndteres asynkront. Så langt, så godt. Eftersom at vi derfor var overbeviste om at p.CloseMainWindow() lige havde kørt og dermed givet anledning til at Exited skulle kaldes, men endnu ikke var kaldt, kom det som noget af en overraskelse at p blev null, hvis vi lavede en inspektion via pop-up-featuren i Visual Studio. Faktisk så p ud til at være ok, indtil vi foldede felter og properties ud. Mange af dem blev reporteret som værende null og hvis vi så efterfølgende kiggede på p igen var den også null. Med andre ord så det ud som om at p simpelthen faldt fra hinanden for øjnene af os!

Hvordan pokker kunne p blive null, når Exited() tydeligvis ikke havde kørt? Faktisk var denne opdagelse så overraskende, at jeg vil anbefale dig, kære læser, at stoppe læsningen af dette indlæg for en stund og udføre ovenstående øvelse, så du selv kan se fænomenet. Det er ret alarmerende.

Jeg har faktisk optalt problemet før, men jeg må indrømme, at jeg havde undervurderet konsekvenserne af denne feature. Problemet viser sig nemlig at være knyttet til Visual Studios evne til at vise værdier i pop-ups, når man fører musemarkøren hen over identifiers i koden. For at kunne vise værdien af properties, er Visual Studio nødt til at afvikle den underliggende kode (faktisk viser Call Stack-vinduet, at kode er blevet udført på denne måde, se efter FuncEval).

Da p i vores tilfælde peger på en instans af Process, der har en hel skov af properties og sikkert også nogle ToString()-metoder, er Visual Studio nødt til at afvikle koden for disse for at få deres resultat. Hvad jeg ikke var klar over er, at den ved samme lejlighed også giver vores tråd lov til at behandle den indkomne event, der forårsager, at vores callback-metode bliver kaldt. I vores tilfælde vil det sige, at når vi fører musen over p og klikker på det lille plus for at se de forskellige properties, bliver Exited() afviklet som reaktion på at Notepad er blevet lukket.

Hvordan skal Visual Studio håndtere breakpoints i kode, der bliver afviklet på den måde? Det er ikke et svar til det spørgsmål, der passer til alle situationer, og faktisk er Visual Studios opførsel også afhængig af omstændighederne. Bliver koden afviklet som en bivirkning af properties-inspektion via musemarkøren eller via Watch-vinduet, ignorerer både VS2005 og VS2008 eventuelle breakpoints. Hvis det derimod er en handling fra Immediate-vinduet, der forårsager afvikling af kode, respekteres eventuelle breakpoints i koden. Så hvis vi efter at være stoppet på CloseMainWindow(), udfører denne med F10 og derefter går til Immediate-vinduet og inspicerer en property som f.eks. p.HasExited, stopper koden faktisk på vores breakpoint i Exited().

Så skønt vi kan forklare den mærkværdige opførsel, ændrer det ikke ved, at konsekvenserne af denne feature med sikkerhed vil komme bag på mange. Uanset hvordan vi vender og drejer det, er det ikke fedt, at breakpoints kun virker for det meste. Til al held kan vi dog slå denne feature fra, hvis vi har brug for at debugge kode, der ikke tåler denne ekstra opmærksomhed fra debuggeren. Under Tools > Options > Debugging > General er der et felt ved navn Enable property evaluation and other implicit function calls. Hvis vi slår den fra, opfører vores kode sig pludselig langt mere forudsigelig.

Rendezvous-teknikker

Thursday, September 27th, 2007

Dette er tredje indlæg i min lille serie om korrekt brug af tråde baseret på Jeffrey Richters glimrende præsentation på den nylig afholdte Devscovery. I første indlæg så vi på omkostninger ved brug af tråde, og andet indlæg handlede, om hvordan vi kan bruge den indbyggede threadpool til at sikre optimal udnyttelse af de tilgængelige ressourcer. Threadpoolen giver os mulighed for at sætte CPU- eller I/O-opgaver over via BeginXxx()-metoderne på delegates og en lang række I/O-relaterede typer. Da en sådan opgave bliver udført asynkront af en tråd i threadpoolen, vil vi ofte være interesseret i at blive underrettet, når opgaven er løst.

Der er sådan set tre måder, vi kan komme i spil igen, efter et job er blevet afviklet af en tråd i threadpoolen, men to af dem er så langt fra det optimale, at der reelt kun er et valg. Jeg vil dog gennemgå alle tre alligevel for overblikket og argumentationens skyld.

Den første og letteste metode er at kalde EndXxx(), når vi er klar til at modtage resultatet fra en asynkron operation. Hvis tråden har færdiggjort sit arbejde, får vi resultatet, og alt er godt. Er den derimod ikke færdig, er vores egen tråd nødt til at blokere, mens den venter på resultatet. Da en af vores målsætninger er, at tråde ikke skal blokere, er denne teknik naturligvis ikke velegnet, med mindre vi altid er sikre på, at vores opgaver når at køre til ende, før vi kalder EndXxx(). Det vil sjældent være tilfældet, og derfor er denne teknik ikke særlig optimal.

Den anden teknik er at bruge IsCompleted på det IAsyncResult, vi får fra BeginXxx(). Via denne property kan vi løbende forespørge på status af vores asynkrone operation. Er opgaven ikke færdig, kan vi lave noget andet, indtil vi spørger igen. Det forudsætter naturligvis, at vi har nok at lave på det pågældende sted i koden til at udfylde ventetiden. Det bliver hurtigt bøvlet, og risikoen for, at vi reelt kommer til at stå at vente på den anden tråd, er overhængende.

Derfor er den eneste brugbare rendezvous-teknik i realiteten at bruge en callback-metode. Alle BeginXxx()-varianterne tilbyder, at vi kan specificere en callback-metode af typen AsyncCallback samt et argument til denne.

Der er flere måder at gøre dette på, men inden vi kommer så langt, vil jeg lige bruge et øjeblik på at se på, hvad der sker. Når vi sætter en opgave over med BeginXxx(), kan vi angive en metode, som vi ønsker kaldt, når opgaven er gennemført. Denne metode vil altså blive kørt efter vores opgave, så der er ikke behov for synkronisering omkring eventuelt delte variabler mellem opgavetråden og callback-tråden. Det er ikke dårligt.

Syntaksen for kald af BeginXxx() med en callback-metode er som følger. Her er et eksempel med BeginInvoke() på en delegate. Principperne er de samme for I/O-opgaver, men vi skal naturligvis huske at åbne den pågældende I/O-ressource for asynkrone operationer.

WorkerDelegate worker = new WorkerDelegate(DoWork);
worker.BeginInvoke(”input”, WhenDone, worker); 

hvor WorkerDelegate er erklæret som:

delegate void WorkerDelegate(string input);

og WhenDone() ser ud som følger:

public static void WhenDone(IAsyncResult res) {
   Console.WriteLine("done ...");
   WorkerDelegate worker = (WorkerDelegate)res.AsyncState;
   worker.EndInvoke(res);
}

Det første parameter til BeginInvoke() er input til DoWork(). I dette tilfælde er det blot en tekst, men vi kunne naturligvis indkapsle al den data, vi har brug for i en passende type. Det andet parameter er en reference til vores callback-metode. Det sidste parameter til BeginInvoke() er input til callback-metoden. Som minimum skal den have en reference til vores delegate-instans, så vi kan kalde EndInvoke() som forventet. Vi kan efterfølgende grave vores parametre frem via det aktuelle IAsyncResult.

Ovenstående fungerer fint, men i mange tilfælde kan vi have behov for at lade DoWork() og WhenDone() dele et data. Vi kan enten pakke disse sammen med delegate-referencen i en type og bruge dem som parameter, lade dem være felter på DoWork()/WhenDone()s type eller benytte C#s mulighed for anonyme delegates. Her er ovenstående omskrevet med en anonym delegate:

WorkerDelegate worker = new WorkerDelegate(DoWork);
worker.BeginInvoke("input", delegate(IAsyncResult res) {
   Console.WriteLine("done ...");
   worker.EndInvoke(res);
}, null);

Som bekendt implementerer C# compileren en anonyme metode ved at oprette en inner class med den tilsvarende metode og reference til alle lokale variable. Det betyder, at koden i vores anonyme delegate uden videre kan tilgå vores worker reference, så vi har ikke længere brug for at levere denne som parameter til WhenDone().

Fordelene ved denne metode er, at man slipper for at håndtere delte variable, og al logikken er samlet et sted. Det giver noget mere overskuelig kode, men det er desværre ikke uden begrænsninger. Eksempelvis er der visse sproglige konstruktioner som f.eks. using, der ikke kan anvendes med ovenstående, grundet den måde compileren ekspanderer anonyme delegate-metoder. Richter har en løsning på dette, men den kræver sin egen gennemgang.

Tråde og skalerbarhed

Wednesday, August 29th, 2007

I dette indlæg vil jeg se på, hvordan C# understøtter multithreaded applikationer, samt hvordan vi kan udnytte de forhåndenværende ressourcer optimalt. Som nævnt er tråde dyre, så som hovedregel bør vi ikke lave flere tråde end nødvendigt, og alle tråde bør køre hele tiden. Tilsammen betyder disse anbefalinger, at det optimale er at have en tråd per tilgængelig CPU. Hvis der er flere tråde, vil de kæmpe om CPUen med tilhørende context switches til følge og dermed dårligere ydelse. Hvis der er færre tråde, vil maskinens potentiale ikke komme til sin ret. Ergo, en tråd per CPU er det optimale.

I C# er tråde direkte repræsenteret via klassen Thread i System.Threading. Hver instans repræsenterer en tråd i Windows, men selve bindingen til en operativsystemtråd kommer først, når instansens Start()-metode kaldes. Thread-klassen understøtter de velkendte operationer for tråde, og selve APIet er beskrevet i hjælpeteksten, så det vil jeg ikke gennemgå i detaljer her, men blot nøjes med at nævne nogle facetter ved tråde, der er relevante i forhold til den efterfølgende diskussion.

  1. Som udgangspunkt afvikles alle tråde med Normal-prioritet, men via Thread-klassens Priority-property kan dette ændres til højere eller lavere prioritet. Jo højere prioritet desto hyppigere vil Windows sætte en tråd til at køre. En tråds prioritet er relativ til processens prioritet, så en højprioritetstråd i en lavprioritetsproces har lavere absolut prioritet end en højprioritetstråd i en højprioritetsproces. Windows reserverer de højeste prioriteter til systemfunktioner.
  2. CLRen skelner mellem to typer tråde Foreground og Background. Disse typer har intet med prioritet at gøre, men vedrører udelukkende hvordan CLRen opfører sig i forhold til afvikling af processer. CLRen vil holde en proces i live så længe, der er en eller flere aktive Foreground-tråde. Hvis der ikke er aktive Foreground-tråde, vil CLRen nedlægge processen uanset hvor mange Background-tråde, der end må være aktive på det aktuelle tidspunkt.
  3. En kørende tråd kan afbrydes via et kald til Abort(). Dette indskyder en ThreadAbortException som den kørende tråd kan reagere på, men ikke ignorere. Det er således muligt, at afbryde en kørende tråd.

Med disse specifikke detaljer på plads, kan vi afslutte gennemgangen af Thread-klassen med at sige, at den i det store hele implementerer de forventede metoder og opfører sig som litteraturen på området beskriver. Hvad, der måske ikke er klart for de fleste, er, at vi som udgangspunkt ikke bør bruge Thread-klassen.

Thread-klassen mangler nemlig midler til, at vi på en let måde kan skalere antallet af tråde i forhold til den konkrete maskine. Det betyder, at hvis vi skal overholde ovenstående anbefaling skal vi selv stå for den fornødne bogføring. Det kan blive særdeles kompliceret, og til al held kan vi undgå det, da CLRens indbyggede Threadpool kan gøre det for os. Threadpoolen er et særdeles anvendelig alternativ til manuel håndtering af tråde.

Threadpoolen er tilgængelig på to måder: Direkte via Threadpool-klassen og indirekte via BeginXxx() og EndXxx()-metoderne på en lang række klasser. Den direkte måde er let at anvende, men har en del begrænsninger. Hvis vi ønsker at sætte en opgave i gang, kan vi kalde en af QueueUserWorkItem()-metoderne med en reference til vores metode samt eventuel reference til et argument. Eksempelvis sørger nedenstående for, at vores metode DoWork() bliver kaldt med argumentet 42:

ThreadPool.QueueUserWorkItem(DoWork, 42);

Det bliver ikke meget simplere, men den simple tilgang kommer desværre også med en række ulemper. F.eks. er det ikke umiddelbart muligt, at få eventuelle resultater ud af DoWork(), og det er heller ikke muligt at konstatere, hvornår metoden er kørt til ende. Det begrænser anvendeligheden af denne fremgangsmåde ganske betragteligt, og derfor er den indirekte metode langt mere interessant.

Når vi erklærer en delegate-type, tilføjer compileren BeginInvoke() og EndInvoke()-metoder, så vi kan kalde metoder af denne type asynkront. Hvis vi eksempelvis har:

delegate int IntDelegate(object state);

Kan vi kalde en metode af denne type asynkront på følgende måde:

IntDelegate worker = DoWork();
IAsyncResult res = worker.BeginInvoke(42, null, null);

Som jeg tidligere har omtalt, skal den tilhørende EndInvoke() kaldes som oprydning for at sikre, at interne CLR-resourcer bliver ryddet op. Via IAsyncResult har vi adgang til såvel returværdi og eventuel exception fra DoWork(). Vi har også flere muligheder for at få besked når vores asynkrone metode er afviklet. Det vil jeg se nærmere på i næste indlæg.

Ovenstående er den ideelle metode til asynkron afvikling af CPU-bundne metoder. For I/O-bundne operationer kan vi bruge en lignende fremgangsmåde, idet størstedelen af I/O-resourcerne i .NET tilbyder et tilsvarende sæt BeginXxx() og EndXxx()-metoder. F.eks. tilbyder FileStream-klassen asynkron læsning af filer via BeginRead()-metoden. Det kræver dog, at den aktuelle stream er åbnet asynkront via en af de mulige constructors. Syntaksen er ikke videre elegant, da vi er nødsaget til at specificere en del flag, der normalt er implicitte. Nedenstående læser indholdet af file.dat asynkront i bidder af 8 KB.

FileStream fs = new FileStream(@"c:tempfile.dat", FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 8 * 1024, FileOptions.Asynchronous);
byte[] data = new byte[fs.Length];
IAsyncResult ar = fs.BeginRead(data, 0, data.Length, null, null);

// her afvikles anden kode

int bytesread = fs.EndRead(ar);
Console.WriteLine("bytes read: {0}", bytesread);

Uanset om vi anvender den direkte eller indirekte metode, står Threadpoolen for al håndtering af nødvendige tråde. Som nævnt er Threadpoolen smart, så den vil som udgangspunkt ikke oprette flere tråde, end der er tilgængelige CPUer (forudsat at de igangsatte tråde ikke blokerer). Det betyder, at vi automatisk får optimal skalering til den konkrete hardware. Det optimale er at designe alle opgaver, så de aldrig blokerer, men i fald en tråd blokerer, vil Threadpoolen oprette yderligere tråde, så CPUens resourcer ikke spildes, mens der ventes på den blokerede tråd. Eventuelle overskudstråde som følge af dette vil automatisk blive nedlagt efter ca. 2 minutters inaktivitet. Threadpoolen er måske ikke perfekt, men den gør et glimrende job, og den befrier os for en masse kedelig logistik.

I langt de fleste tilfælde er det bedst, at anvende ovenstående metoder, da det sikrer den bedst mulige ydelse på den aktuelle maskine, men der er undtagelser. Der er få tilfælde, hvor vi med fordel bør kaste os ud i manuel håndtering af tråde:

  1. Hvis opgaven skal køre meget lang tid. Threadpoolen vurderer konstant behovet for at skabe yderligere tråde og regner som udgangspunkt med at alle tråde afvikler kortere opgaver. I nogle tilfælde kan det derfor give mening, at fritage Threadpoolen for den belastning en langtidskørende tråd kan repræsentere.
  2. Hvis tråden skal have anden prioritet end Normal. Alle tråde i Threadpoolen kører med Normal-prioritet, og skønt vi kan ændre prioriteten for den enkelte tråd, har vi ikke mulighed for at bestemme hvilken tråd en given opgave afvikles på, og derfor bliver det let unødig kompliceret at forsøge at styre dette element for tråde i Threadpoolen.
  3. Hvis tråden ikke må være en Background Thread. Alle tråde i Threadpoolen er Background-tråde, hvilket betyder, at de vil blive nedlagt når processen bliver nedlagt.
  4. Hvis tråden skal kunne afbrydes, inden den er kørt til ende. Threadpoolen pakker selve håndteringen af tråde ind, så vi har ikke umiddelbart adgang til de samme kontrolmekanismer for de enkelte tråde.

Dokumentationen nævner også, at man skal undgå at bruge Threadpoolen til opgaver, der har brug for at blokere i længere tid. Det kan jeg ikke være uenig i, men anbefalingen er i realiteten at vi helst ikke skal have tråde, der blokerer.

Tråde og omkostninger

Monday, August 20th, 2007

Som nævnt var højdepunktet ved Devscovery Jeffrey Richters heldagsseance omkring threading, og derfor vil jeg i de næste par indlæg gennemgå hans væsentligste pointer. Meget af det, han fremlagde, havde jeg set før, men han fik skabt en kontekst, der fik undertegnede til at se på threading med helt andre øjne.

Jeg har efterfølgende genlæst hans kapitler om threading i CLR via C#, og skønt han fremlægger mange af de samme pointer, fremstår de i mine øjne langt fra så tydelige som de gjorde på konferencen. Problemet omkring threading er i en nøddeskal, at tråde er dyre, og derfor bør man undgå dem. På den anden side, er tråde en nødvendighed for at udnytte flere CPUer samt et middel til at lave robuste applikationer. Udfordringen er således at finde den til enhver tid korrekte afvejning. I dette indlæg vil jeg se på omkostningerne ved tråde.

Tråde er dyre. De er billigere end processer, men stadig meget dyre. I praksis kan det let tage op til et par sekunder at starte en ny tråd. Det lyder selvfølgelig ikke af meget, men det svarer til grotesk mange clock cycles, så for en applikation er det en høj omkostning. Lad os se lidt på, hvorfor tråde er så dyre.

Tråde er både et Windows- og et CLR-begreb. I C# er tråde repræsenteret ved klassen Thread i System.Threading. Dette er blot en letvægtsstruktur, der repræsenterer en tråd i Windows-regi. Det er stort set omkostningsfrit at oprette en Thread-instans i C# (det er blot et objekt på heapen). Det er først, når man kalder Start() på instansen, at CLRen beder Windows om at oprette en rigtig tråd, og det er her prisen betales. Når der oprettes en tråd sker følgende:

  1. Hver tråd har en såkaldt kontekst, der bruges til at opbevare de aktuelle registerværdier for tråden. Størrelsen af denne er processorafhængig. For x86 drejer det sig om ca. 700 bytes. For IA64 ca. 2500 bytes.
  2. Hver tråd har desuden en user mode-stak og en kernel mode-stak på henholdsvis 1 MB og 12 KB (24 KB i 64 bit Windows). Disse afsættes som commited memory, så snart tråden oprettes i operativsystemet.
  3. Når en tråd oprettes, sender Windows et signal om dette til samtlige DLLer i den aktuelle proces. Det kan let betyde op til et par hundrede funktionskald, og hvad værre er, vil flere af disse ofte medføre paging for at få de nødvendige kodestumper indlæst. Det samme sker, når tråden nedlægges.

Det vil altså sige, at hver tråd optager lidt over en megabyte, uanset om tråden laver noget eller ej. Med minimum 512 MB i de fleste maskiner i dag er det naturligvis ikke det store, men givet at mange applikationer i den grad sviner med antallet af tråde, er det ikke ualmindeligt at have flere hundrede tråde kørende på en gang (efter en mindre oprydning er jeg nede på 443 tråde, mens jeg skriver dette). Det er meget hukommelse for ingenting. Det kan f.eks. være svært at forstå, hvorfor winlogon.exe har ikke mindre end 18 tråde i spil (under XP – i Vista er antallet dalet til tre hvilket må siges at være langt bedre). Alle disse tråde bidrager til et øget memoryforbrug, og dermed en dårligere ydelse for hele systemet. Når vi tilmed tager i betragtning, at CPUen ofte er ledig i 90-95% af tiden på en almindelig brugermaskine, betyder det, at størstedelen af trådene ikke laver noget som helst (udover at optage en masse hukommelse). Da hverken applikationerne eller systemet som helhed har glæde af inaktive tråde, ville alle være bedre tjent med færre tråde.

Hvis der er mere end en tråd per CPU, sørger Windows for at der hyppigt skiftes imellem disse, så det ser ud som om, at de alle kører samtidig. Dette skift kaldes et context switch, og det er ligeledes en dyr operation. Ca. hvert 20 millisekund afbryder Windows den kørende tråd for at lade en anden komme til. For at dette kan lade sig gøre, skal Windows i kernel mode og den kørende tråds tilstand skal gemmes. Det vil sige, at CPU-registre gemmes i den tilhørende kontekst. Derefter skal Windows finde ud af hvilken tråd, der nu skal have lov at køre og efterfølgende hente dens kontekst ind i CPU-registrene. Til slut forlades kernel mode igen. Alt dette sker ca. hvert 20 millisekund, hvis der er mere end en tråd per CPU. Det burde være indlysende, at dette overhead ikke gavner applikationens ydelse. Det optimale er således, at der kun er en tråd per CPU, idet det sikrer den største mulige parallelle afvikling.

Så for at opsummere får vi altså den optimale udnyttelse af tråde, hvis vi begrænser os til kun at have en tråd per CPU, da det giver det mindste hukommelsesforbrug i forhold til udnyttelsesgraden, idet vi derved slipper for context switches i den aktuelle proces (Windows skifter naturligvis også mellem tråde i forskellige processer, men den del kan vi ikke kontrollere i en given applikation).

I næste indlæg vil jeg se på, hvordan man sikrer sig det optimale antal tråde i en applikation.

Managed Leak

Tuesday, March 27th, 2007

Note (5. marts 2008): Jeg har skrevet en opdatering vedrørende problemet i dette indlæg.

Det er måske nærliggende at tro, at C# applikationer ikke kan forårsage memory leaks. Hvordan skulle de kunne det, når applikationen har overdraget ansvaret for håndtering af hukommelsen til afviklingsmiljøet? Objekter allokeres på managed heap, og når der er brug for lidt oprydning, træder garbage collectoren til og rydder op på passende vis. Det hele gøres let og elegant og med imponerende resultat.

Definitionen på et leak er, at en applikation konsumerer ressourcer over tid uden nogensinde at frigive disse. På sigt kan det føre til, at applikationen og tilmed hele systemet kommer til at lide under denne grådighed. Den slags problemer er desværre ikke umulige i C#. Der er flere eksempler på konstruktioner, der optager ressourcer uden nogensinde at frigive dem igen. C#s model for asynkron afvikling af funktioner, får i visse tilfælde CLRen til at leake.

C# gør det let at få metoder afviklet asynkront via Delegate-klassen, der er en speciel klasse som compileren bruger til at implementere asynkrone metoder. Man opretter blot en delegate-metode og kalder den compiler-genererede BeginInvoke-metode på en instans af delegate-typen, så klarer C# resten.

public delegate void DoSomethingDelegate();
DoSomethingDelegate d = DoSomething;
IAsyncResult res = d.BeginInvoke(null, null);

// lav noget andet

d.EndInvoke(res);

Ovenstående erklærer en delegate-type ved navn DoSomethingDelegate og metoden DoSomething af denne type. DoSomething kan afvikles asynkront som vist ovenfor. Hvis EndInvoke kaldes, inden metoden er kørt til ende, blokerer den aktive tråd og venter på, at den asynkrone metode bliver færdig (svarende til at metoden var blevet kaldt synkront). Hvis vi forestiller os, at hvad end DoSomething laver, er vi ikke interesseret i at risikere at komme til at vente på den. Vi vil bare kunne sætte metoden i gang og lade den køre uden at bekymre os yderligere om den – med andre ord: vi ønsker os en ”fire and forget”-funktionalitet.

DoSomethingDelegate d = DoSomething;
d.BeginInvoke(null, null); // bare kør metoden

Koden ovenfor gør det ønskede: DoSomething kører og gør, hvad den skal for derefter at stoppe, og vi hører aldrig mere fra den. Desværre får ovenstående CLRen til at leake. Når der oprettes et asynkront kald via BeginInvoke, afsætter CLRen nemlig en intern struktur til håndtering af diverse parametre for det kaldte metode. Denne interne struktur frigives først, når EndInvoke kaldes med den relevante IAsyncResult. Ergo, hvis BeginInvoke kaldes, skal den tilhørende EndInvoke også kaldes, ellers vil CLRen aldrig frigive de afsatte ressourcer. Da der er tale om interne CLR-ressourcer, kommer de ikke i betragtning i forbindelse med garbage collectorens oprydning og derfor er der tale om et memory leak. Microsofts dokumentation understreger da også, at enhver BeginInvoke skal efterfølges af en tilhørende EndInvoke, men de gør det ikke klart hvorfor. At undgå et memory leak er en ret god grund til at sørge for at rydde op efter sig.

EndInvoke skal som nævnt kaldes med den korrekte IAsyncResult, hvilket vil sig det specifikke IAsyncResult BeginInvoke returnerede. Det betyder, at applikationen skal holde styr på sammenhæng mellem kald af BeginInvoke og EndInvoke (og de skal naturligvis begge kaldes via samme instans af delegate-typen). Kaldes EndInvoke med et forkert IAsyncResult kastes en exception.

Så skønt vi kan have behov for at udføre en funktion asynkront og se bort fra udfaldet i nogle tilfælde, er vi under alle omstændigheder nødt til at afslutte forløbet ved pænt at kalde EndInvoke som forskrevet. For at implementere ”fire and forget”-funktionalitet, skal der altså mere til, og problemet kompliceres af, at EndInvoke skal kaldes med den korrekte IAsyncResult.

Problemet kan løses ved at lave en klasse, der pakker den nødvendige oprydning ind som f.eks.:

public class FireAndForget {
   // typefast metode til kalde af fire-n-forget-funktion
   public delegate void InvokeDelegate();
   public static void Invoke(InvokeDelegate d) {
      Invoker.BeginInvoke(d, null, CleanUp, null);
   }

   // indpakning til oprydning
   private delegate void InvokerDelegate(Delegate d, object[] args);
   private static InvokerDelegate Invoker =
      new InvokerDelegate(InvokeWrapper);
   private static void InvokeWrapper(Delegate d, object[] args) {
      d.DynamicInvoke(args);
   }

   // oprydning
   private static AsyncCallback CleanUpDelegate =
      new AsyncCallback(CleanUp);
   private static void CleanUp(IAsyncResult res) {
      Invoker.EndInvoke(res);
   }
}

Hvis SomeFunction er af typen InvokeDelegate, kan vi afvikle den på fire-and-forget-vis som følger:

FireAndForget.Invoke(SomeFunction);

Ideen i ovenstående er at pakke fire-and-forget-kaldet ind i et asynkront kald med den nødvendige oprydning. Det asynkrone kald sørger derefter for at kalde den ønskede fire-and-forget-metode. Oprydningen sker via BeginInvokes call back-funktionalitet.

BeginInvoke tillader, at brugeren angiver en metode af typen AsyncCallback, der skal kaldes, når det asynkrone kald, BeginInvoke igangsætter, er færdig. Som nævnt ovenfor er det nødvendigt, at bruge det korrekte IAsyncResult til oprydningen med EndInvoke. Det er BeginInvoke-kaldet, der returnerer dette IAsyncResult, så hvordan pokker får man overleveret dette til call back-metoden i et og samme kald? Til al held er det ikke nødvendig. Afviklingsmiljøet sørger nemlig selv for at call back-metoden får adgang til det relevante IAsyncResult og derfor fungerer ovenstående.

I stedet for at erklære InvokeDelegate og bruge denne til at implementere en typefast Invoke-metode kunne Invoke erklæres med et parameter af typen Delegate. Det ville dog kræve, at brugeren erklærer en relevant delegate-type og opretter instanser af denne type. Det ville gøre ovenstående indpakning lidt simplere på bekostning af lidt mere bøvl ved brug af klassen.