Archive for the ‘AppDomain’ Category

Zombieinvasion

Wednesday, August 5th, 2009

Det er muligvis bare mig, der har fine fornemmelser, men jeg synes, at man bør genoverveje sin ide, når fremgangsmåden fostrer en hær af zombier.

I skrivende stund er der et ret interessant, accepteret svar til følgende spørgsmål på StackOverflow.com. Spørgeren er interesseret i at lave en applikation, der kan huse andres kode på en robust måde. Han er således ikke interesseret i, at applikationen går ned, hvis gæstekoden ikke opfører sig ordentlig. Ikke overraskende er spørgeren kommet på sporet af AppDomains og har tilmed fundet frem til AppDomain.UnhandledException. Desværre gør den ikke det ønskede.

Problemet er som følger: Gæstekoden kan lave tråde, og disse kan smide exceptions. Hvis gæstekoden ikke selv håndterer disse exceptions, vil applikationen blive stoppet som følge af en uhåndteret exception.

Som dokumentationen beskriver, giver AppDomain.UnhandledException nemlig kun mulighed for oprydning og notifikation. Når event-håndteringen er kørt til ende, vil applikationen blive afsluttet som følge af den uhåndterede exception.

Fra og med version 2.0 af CLRen har det været Microsofts politik, at alle uhåndterede exceptions behandles på denne måde, og derfor er der ikke rigtig noget at gøre. Har man brug for anden opførsel (som det kendes fra f.eks. SQL Server og IIS), er man nødt til at implementere sin egen CLR-værtsproces. Gør man det, har man mulighed for at specialisere, hvordan disse og mange andre aspekter skal håndteres. Alternativt kan man sætte sin CLR til at køre i version 1.1 compatibility mode, men det vil næppe heller være acceptabelt i alle situationer.

Zombiekode

Det bringer os videre til det accepterede svar. Jeg har forsimplet koden en smule men i en nøddeskal, ser event-håndteringen ud som følger:

static void CatchAllHandler(object sender, UnhandledExceptionEventArgs args) {
   Thread.CurrentThread.IsBackground = true;
   while (true) {
      Thread.Sleep(TimeSpan.FromHours(1));
   }
}

Sætter man ovenstående handler på AppDomain.UnhandledException, vil applikationen tilsyneladende overleve hvad som helst (almindelige exceptions i hvert fald – som jeg har beskrevet tidligere, er der visse exceptions, der ignorerer den konventionelle håndtering).

Men inden vi glæder os for meget, er det værd lige at tænke over, hvad koden gør. Hvis det ikke allerede står klart, så brug lige et par sekunder på at tænke over ovenstående …

Ser du det samme som jeg? Hvis logikken er, at applikationen lukkes, så snart event-håndteringen er afsluttet, så kan vi da bare lade være med at forlade handleren. Hvordan gør vi det? Med en uendelig løkke!

Er det en god ide at have et antal tråde, der ikke laver andet og aldrig kommer til at lave andet end at vente? Al den stund at tråde er en begrænset ressource med et ikke uvæsentligt overhead, synes jeg, at det er en dårlig ide.

Hvis ovenstående ”løsning” får lov at køre længe nok, vil vi i bedste fald have udskudt de fatale problemer til senere. På et eller andet tidspunkt, vil applikationen mærke konsekvenserne af alle zombietrådene. Spørgsmålet er så, om det er bedre at fejle med det samme og rapportere den egentlige årsag end bare at fortsætte med kælderen fuld af zombier?

Hvis du undrer sig over, hvordan IsBackground = true kommer ind i billedet, så skyldes det, at CLRen ikke vil lukke en proces, der har aktive tråde, med mindre disse er baggrundstråde. Så med andre ord: Uden dette tiltag ville zombietrådene holde applikationen i live for altid. Undead party FTW!

Oprydning

Hvad nu hvis vi sørger for at slå zombietrådene ned? Er ovenstående så ikke brugbart? I teorien, måske. Men inden vi kommer så langt, skal vi lige have slået zombierne ihjel, og er der en ting utallige gyserfilm har lært mig, så er det, at den slags ikke er så let endda.

Vi ved, at hvis vi hopper ud af CatchAllHandler(), fortsætter nedlukningen af applikationen som følge af den uhåndterede exception, så hvordan kommer vi af med tråden, uden at dette sker?

Jeg ved ikke, om det er en regel eller en tilfældighed, men mine undersøgelser viser, at CatchAllHandler() kører på den tråd, der oprindelig smed en exception. Ergo, har vi en handle til denne tråd i form af Thread.CurrentThread og kan således kalde Abort() på den. Desværre har det samme effekt som at forlade CatchAllHandler(). Applikationen lukkes med den oprindelige exception. Det er altså ikke en løsning.

Hvad så hvis vi i stedet for eksplicit at slå tråden ned, unloader vores AppDomain? At unloade et AppDomain har den bivirkning, at alle tråde, der enten kører kode i domænet eller har returadresser på stakken, der gør, at de kommer til at køre kode i domænet, bliver slået ned. Det må være godt nok til formålet, så lad os prøve det.

Desværre forholder det sig sådan, at AppDomain.Unload() kan finde på at smide en CannotUnloadAppDomainException. Det gør den f.eks. hvis der kører unmanaged kode, men det gør vi jo ikke.

Desværre kommer denne exception også, hvis en eller flere tråde i domænet ikke kan afbrydes, hvilket er tilfældet, hvis de afvikler kode i en catch eller finally-blok, eller hvis de tilfældigvis er i gang med at køre UnhandledException-handleren. Så med andre ord sker der følgende: Vi forsøger at unloade vores AppDomain, men eftersom at vi har en uendelig løkke i vores handler, venter Unload() i nogle sekunder, hvorefter den giver op og smider en CannotUnloadAppDomainException.

Hvis vi var sikre på, at vores handler på et eller andet tidspunkt ville blive færdig, kunne vi gentage forsøget, indtil det lykkedes, men vi har jo lige konstateret, at ethvert forsøg på at forlade vores handler resulterer i at applikationen lukkes. Derfor vil Unload() af domænet altid fejle.

Med andre ord: Ovenstående handler lader ikke bare den fejlende tråd hænge, den lader også hele vores AppDomain hænge, hvilket er en meget dårlig løsning for en plugin-arkitektur.

Så konklusionen er (stadig): Hvis vi vil lave en applikation, der kan indlæse vilkårlig .NET-kode på en robust måde, er vi nødt til at implementere vores egen CLR-værtsproces.

Uheldige låse

Monday, August 18th, 2008

I mit indlæg om tråde og samtidighedsproblemer skrev jeg lidt om valg af låse til udelt adgang, men der var ikke plads til at komme ind på alle detaljerne. Derfor vil jeg i dette indlæg se lidt nærmere på problematikken.

I C# er udelt adgang implementeret via Monitor-klassen. Metoden Enter(object) tager en instans af en vilkårlig referencetype og bruger denne som lås. Er låsen ledig, får tråden lov til at fortsætte og er derefter sikret udelt adgang til den efterfølgende kode. Er låsen optaget af en anden tråd, bliver den spørgende tråd lagt til at vente, indtil låsen igen er ledig. En lås frigives igen ved at kalde Monitor.Exit(). Der er flere varianter, men det er uvæsentlig for denne diskussion.

Glemmer vi at frigive en lås, kan det lede til alvorlige samtidighedsproblemer, og derfor er det vigtig, at låse også frigives i tilfælde af fejl. Bruger vi Monitor.Enter() og Exit(), skal sidstnævnte således altid kaldes fra en finally-blok. For at gøre det lettere at skrive korrekt kode tilbyder C# som bekendt et lock keyword, der netop implementerer denne fremgangsmåde.

Med denne korte introduktion bag os kan vi nu rette blikket i retning af, hvad der kan og bør låses på. Da Enter() tager en instans af typen object, kan vi jo låse på hvad som helst, men i praksis er der instanser, der er meget uheldige at bruge som låse. Her er en oversigt.

Offentlige instanser: Når vi implementerer udelt adgang, er det nødvendig, at alle tilgange til de resourcer, vi ønsker at beskytte, foregår via en den samme lås. Det er ligeledes vigtig, at vi har fuldstændig styr på, hvor låse bliver tilegnet og frigivet. Derfor giver det næsten sig selv, at det er en dårlig ide, at låse på en instans, der er offentlig tilgængelig, da det jo lukker op for, at andre kan låse på denne instans og dermed introducere samtidighedsproblemer for vores kode.

this: Microsofts kode gør det flere steder, men det ændrer ikke ved, at det er en uskik at bruge lock(this). this er jo netop den ultimative offentlige variabel. Uanset hvad vi gør, kan vi ikke beskytte adgangen til denne, når der først eksisterer instanser af typen.

typeof(): Ligesom this er typeof tilgængelig både i og udenfor vores kode, men hvad værre er, så vil typeof() i nogle tilfælde returnere den samme instans på tværs af AppDomains! Det vil sige, at tråde i et AppDomain kan påvirke tråde i et andet. Det er rigtig skidt, og den slags fejl kan være meget svære at identificere. Hvis et assembly er indlæst som domain neutral, vil alle AppDomains dele typerne i dette assembly og dermed få samme reference som svar på et kald til typeof(). Som udgangspunkt er det kun mscorlib.dll, der indlæses således, så f.eks. vil typeof(int) returnere en reference til samme instans på tværs af AppDomains. Via konfiguration kan vi indlæse andre assemblies, så de bliver delt på denne måde, og dermed vil typeof() give samme problem for typer i disse assemblies.

string: Som jeg tidligere har omtalt, har CLRen en mekanisme kaldet interning, hvis formål er at forsøge at reducere antallet af strings i hukommelsen ved at lade strings med identisk indhold være repræsenteret af referencer til samme instans. Det betyder naturligvis, at vi kan erklære en string private i håb om at ingen andre kan tilgå den, men CLRen vil ikke desto mindre returnerer referencen til præcis denne string, hvis den optræder andre steder i koden. Faktisk er interned strings delt på tværs af ikke bare klasser men også AppDomains, hvilket gør det risikabelt at bruge strings som låse.

Komplekse typer: Der burde ikke være noget galt i at låse på en instans af en vilkårlig privat type, men hvis denne type følger den uheldige praksis at låse på this, kan vores kode føre til deadlocks, hvis vi låser på vores instans. Derfor er det vigtig at være opmærksom på, om typen anvender låse eller ej, inden man låser på en instans.

Værdityper (value types): Forsøger vi at kalde lock på en værditype, får vi en compile-fejl, men kalder vi Monitor.Enter() direkte, er compileren flink nok til at boxe vores værditype. Desværre betyder det jo, at forskellige kald til Enter() vil få hver deres boxed kopi af vores værditype. Heldigvis vil vi dog få en exception, når vi efterfølgende forsøger at kalde Exit() på den samme værdi, idet vi jo forsøger at frigive en lås på endnu en boxed udgave af vores værdi.

Jeg skal ikke afvise, at der er en enkel eller to problematiske konstruktioner, jeg har overset, men i alle tilfælde vidner ovenstående om, at vi ikke bare skal låse på en vilkårlig instans.

Anbefalingen er derfor: Opret en privat instans af object for hvert selvstændig område, hvor udelt adgang er påkrævet og lås på denne. Det er en god ide, at vedtage en navnestandard for disse låseobjekter, så de let kan identificeres.

Introduktion til AppDomains – tredje del

Monday, May 19th, 2008

I de to første indlæg (her og her), har vi set på motivationen for AppDomains samt hvad der i praksis skal til for at få dem i luften. I dette indlæg vil jeg se på nogle af de specielle omstændigheder, der gør sig gældende, når vi arbejder med AppDomains.

Lad os lægge ud med at se lidt på hvordan AppDomains ser ud i afviklingsmiljøet. Til formålet bruger vi en opdateret udgave af det simple eksempel fra forrige indlæg. I sin helhed ser det ud som følger:

namespace SimpleAppDomain {
   class Program {
      static void Main(string[] args) {
         SomeType st = new SomeType("Main");
         Console.WriteLine("In Main with SomeType created in '{0}'", st.CreatedIn);
         AppDomain domain = AppDomain.CreateDomain("New AppDomain");
         domain.DoCallBack(Hello);
         Console.WriteLine("Press <Enter>");
         Console.ReadLine();
         AppDomain.Unload(domain);
      }

      static void Hello() {
         SomeType st = new SomeType("Hello");
         Console.WriteLine("Hello from '{0}'", AppDomain.CurrentDomain.FriendlyName);
         Console.WriteLine("SomeType created in '{0}'", st.CreatedIn);
      }
   }

   class SomeType {
      public SomeType(string createdin) {
         CreatedIn = createdin;
      }

      public readonly string CreatedIn;
   }
}

Hvis vi kører ovenstående og kobler WinDbg på, kan vi inspicere applikationens afviklingsmiljø, som det ser ud ved ReadLine(). Den noget umotiverede brug af SomeType skyldes, at vi har brug for et par let identificerbare instanser på heapen, så vi har noget at se på i WinDbg, når vi skal se på, hvordan AppDomains anvender heapen.

Det første, vi kan gøre, er at se hvilke AppDomains, vi har i spil. !DumpDomain lister AppDomains for den aktuelle proces.

0:007> !dumpdomain
--------------------------------------
System Domain: 7a3bc8b8
LowFrequencyHeap: 7a3bc8dc
HighFrequencyHeap: 7a3bc934
StubHeap: 7a3bc98c
Stage: OPEN
Name: None
--------------------------------------
Shared Domain: 7a3bc560
LowFrequencyHeap: 7a3bc584
HighFrequencyHeap: 7a3bc5dc
StubHeap: 7a3bc634
Stage: OPEN
Name: None
Assembly: 001aa628
--------------------------------------
Domain 1: 00163240
LowFrequencyHeap: 00163264
HighFrequencyHeap: 001632bc
StubHeap: 00163314
Stage: OPEN
SecurityDescriptor: 00159f38
Name: SimpleAppDomain.exe
Assembly: 001aa628 [C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader: 001aa6b0
SecurityDescriptor: 0019d848
  Module Name
790c2000 C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll
009923dc C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\sortkey.nlp
00992050 C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\sorttbls.nlp

Assembly: 001b3e80 [c:\workspaces\AppDomains\SimpleAppDomain\bin\Release\SimpleAppDomain.exe]
ClassLoader: 001b3ae8
SecurityDescriptor: 001af0d0
  Module Name
00972c3c c:\workspaces\AppDomains\SimpleAppDomain\bin\Release\SimpleAppDomain.exe

--------------------------------------
Domain 2: 001bc9e0
LowFrequencyHeap: 001bca04
HighFrequencyHeap: 001bca5c
StubHeap: 001bcab4
Stage: OPEN
SecurityDescriptor: 001bea80
Name: New AppDomain
Assembly: 001aa628 [C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll]
ClassLoader: 001aa6b0
SecurityDescriptor: 001b9148
  Module Name
790c2000 C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll
009923dc C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\sortkey.nlp
00992050 C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\sorttbls.nlp

Assembly: 001d0050 [c:\workspaces\AppDomains\SimpleAppDomain\bin\Release\SimpleAppDomain.exe]
ClassLoader: 001d00d8
SecurityDescriptor: 001cff28
  Module Name
010f2d60 c:\workspaces\AppDomains\SimpleAppDomain\bin\Release\SimpleAppDomain.exe

Læg mærke til, at vi faktisk har fire AppDomains; vores applikation (Domain 1), vores ekstra AppDomain (Domain 2) samt de to system-AppDomains, System Domain og Shared Domain, jeg omtalte i første indlæg. Jeg vil ikke gå i detalje med disse i dette indlæg, men kort fortalt står System Domain for opstart af hele processen. Det vil sige, at den blandt andet indlæser mscorlib i Shared Domain. Derudover er det også i System Domain, at interned strenge opbevares. Shared Domain bruges som navnet antyder til opbevaring af assemblies, der deles mellem forskellige AppDomains. Det vil f.eks. sige alle de basale typer defineret i mscorlib.

Det næste, vi kan gøre, er at kaste et blik på heapen. Det kan vi gøre med !DumpHeap, og for at begrænse output, sætter jeg den til at filtrere på typer, hvis navn indeholder navnet på vores namespace. Det giver følgende:

0:007> !dumpheap -stat -type SimpleAppDomain
total 2 objects
Statistics:
      MT    Count    TotalSize Class Name
010f31ec        1           12 SimpleAppDomain.SomeType
009730c4        1           12 SimpleAppDomain.SomeType
Total 2 objects

Som det fremgår ligger såvel Mains som Hellos instanser af SomeType på heapen, hvilket skyldes, at samtlige AppDomains deler den samme heap. Det lyder måske lidt underligt i forhold til ideen om at AppDomains bruges til at isolere de enkelte delapplikationer fra hinanden, men eftersom managed code ikke har mulighed for at manipulere heapen direkte, er dette helt forsvarligt.

Garbage collection på tværs af AppDomains er dog ikke uden sine udfordringer. Som vi allerede har set på, vil eventuelt delte objekter typisk være baseret på MarshalByRefObject, hvilket vil sige, at det ene AppDomain har en reference til en proxy. Det andet AppDomain vil i mange tilfælde kun have en intern reference til instansen i form af en reference fra det kode, der sørger for at håndtere marshalling mellem de to AppDomains. Hvordan skal denne kode vide, hvornår proxyen ikke længere er i spil? Det ved den heller ikke, og i stedet bruges en timer, en såkaldt lease.

I praksis fungerer det ved, at denne timer holder øje med brugen af det objekt, proxyen repræsenterer. Hver gang dette tilgås, nulstilles timeren og forlænger dermed levetiden af objektet. Hvis timeren overskrider en grænseværdi (som default ca. 5 minutter), antager koden, at proxyen ikke længere er i spil og frigiver herefter referencen, så instansen kan blive ryddet op af garbage collectoren.

Det er naturligvis muligt at justere grænseværdien efter behov. MarshalByRefObject definerer en virtuel metode InitializeLifetimeService, der returnerer et lease-objekt. For at ændre grænseværdien, skal vores type blot overstyre denne metode til at returnere en lease med den ønskede værdi. Hvis vi helt vil frakoble denne mekanisme og dermed selv tage ansvaret for, at instanserne bliver ryddet op, skal vi blot returnerer null.

Men hvordan sørger vi for, at et objekt bliver ryddet op i en managed applikation? Vores eneste indflydelse på oprydningen er at kalde GC.Collect(), men det igangsætter blot en oprydning. Hvis vores instans stadig er refereret – og det er den jo i dette tilfælde – gør det ingen verdens forskel. Så hvordan kommer vi af med disse udødelige instanser?

Svaret er, at alle instanser naturligvis kan opryddes, når AppDomainet ikke længere refererer dem. Ergo, bliver disse instanser først fjernet fra heapen, når vi nedlægger vores AppDomain, hvilket vi som illustreret tidligere kan gøre med AppDomain.Unload().

Så for at afrunde dette indlæg på passende vis, kan vi se på, hvad der sker, når vi nedlægger et AppDomain. Det åbenlyse er naturligvis at AppDomainet og alle dets lokale assemblies frigives. Det knap så indlysende er, at vi også risikerer at miste et antal tråde ved samme lejlighed. Når et AppDomain unloades, inspiceres kaldestakken for samtlige managed tråde og hvis en eller flere af disse har returadresse i det AppDomain, der nedlægges, indskydes en ThreadAbortException på de pågældende tråde. En ThreadAbortException kan fanges, men CLRen sørger selv for at smide den igen, så i praksis betyder det, at disse tråde vil blive nedlagt.

Fordelen ved dette er naturligvis at vi ikke risikerer, at en tråd pludselig hopper tilbage i det nu nedlagte AppDomain, men er man ikke klar over denne opførsel, kan det naturligvis komme som lidt af en overraskelse. Derfor er det vigtig, at vi har styr på hvilke tråde, der afvikler kode i specifikke AppDomains. Har vi det, er problemet ikke så stort endda.

Der er stadig flere detaljer, vi ikke har gennemgået, men jeg håber, at denne lille serie ikke desto mindre har givet en brugbar introduktion til AppDomains i C#.

Et WTF?!-øjeblik

Friday, May 16th, 2008

En kollega og jeg sad og kiggede på indlæsning af assemblies i et AppDomain i dag, og faldt over en lidt besynderligt observation. Betragt nedenstående kode:

AppDomain domain = AppDomain.CreateDomain("Container");
domain.Load(SomeAssembly);

foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) {
   Console.WriteLine(assembly.FullName);
}

Spørgsmålet er simpelt: Optræder det assembly, der er repræsenteret ved SomeAssembly i listen over assemblies indlæst i CurrentDomain? Og hvorfor/hvorfor ikke?

Svaret er ja, og det kom unægtelig bag på os. Syntaksen mere end antyder, at SomeAssembly indlæses i vores til lejligheden oprettede AppDomain, men det er bare ikke tilfældet [1].

MSDN-dokumentationen er da også helt klar på dette punkt (tool tip-hjælpen siger derimod intet om de specielle forhold her). Load() indlæser nemlig altid assemblies i det nuværende AppDomain. Instansvarianten er udelukkende tilstede som hjælp for “interoperability callers”, der af en eller anden grund ikke kan kalde den statiske variant.

Så tak til dokumentationen, men er jeg den eneste, der ville have foretrukket et andet navn til denne metode? Og om ikke andet så et lidt mere uddybende tool tip.

[1] Det indlæses faktisk i domain, men det indlæses også i CurrentDomain, og det var det, der kom som lidt af en overraskelse for os.

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.

Introduktion til AppDomains – første del

Tuesday, April 15th, 2008

AppDomains er et af de områder af .NET, jeg indtil nu ikke har brugt meget tid på. Jeg læste om dem, da jeg begyndte at arbejde med C# men konkluderede hurtig, at det næppe var et værktøj, jeg ville få brug for i mit daglige arbejde. Den antagelse har holdt stik indtil videre, men nu er jeg blevet involveret i et projekt, hvor vi overvejer at bruge AppDomains, og derfor har jeg brugt nogle dage på at studere og lege med AppDomains. Dette er første indlæg i en lille serie om AppDomains.

For de fleste selvstændige C#-applikationer vil der typisk ikke være brug for AppDomains – her vil selve processen udfylde de nødvendige ansvarsområder, og vi slipper således for yderligere kompleksitet som følge af flere AppDomains. For applikationer hvis opgave det er at hente og afvikle delprogrammer on-the-fly som f.eks. en ASP.NET-server er AppDomains derimod ret interessante.

I teorien er der intet i vejen for, at en applikation kan hente delprogrammer og afvikle dem uden brug af AppDomains. Vi kan starte et antal tråde svarende til de delapplikationer, vi ønsker at afvikle, og CLRen sørger selv for at hente de assemblies, vi skal bruge, når vi har brug for dem. Desværre kræver det, at alle assemblies skal være kendt på forhånd, og der er ingen mekanismer, der forhindrer vores delapplikationer i at træde hinanden over tæerne. Hvis to eller flere tråde tilgår de samme ressourcer, har vi desuden brug for synkronisering, hvilket ikke alene rammer performance, men det giver os også en hel bunke ubehagelige problemer at adressere. Et andet væsentlig problem er, at når først CLRen har indlæst et assembly, hænger vi på dette, så længe vores applikation kører. Hvis vi ønsker at hente og afvikle mange forskellige applikationer, kommer vi således til at hænge på foreningsmængden af nødvendige assemblies i hele applikationens køretid.

Her kommer AppDomains ind i billedet. AppDomains giver os følgende:

  1. Isolering – et AppDomain kan ikke uden videre tilgå et andet, så behovet for synkronisering forsvinder
  2. Dynamisk håndtering af assemblies – assemblies kan indlæses og droppes igen efter behov, så vi slipper for at slæbe rundt på dødvægt fra assemblies, vi ikke længere har brug for
  3. Individuel konfiguration og sikkerhedsopsætning – hvert AppDomain kan konfigureres, så dynamisk indlæste assemblies f.eks. ikke har samme rettigheder som selve applikationen

At dømme ud fra ovenstående har AppDomains en del tilfælles med processer, og vi kan da også tænke på AppDomains som en slags CLR-processer, men AppDomains ikke er et alternativ til processer. Windows kender intet til AppDomains, så fra operativsystemets synspunkt er alle .NET-applikationer ens – AppDomains eller ej.

Alle .NET-applikationer har mindst et AppDomain kaldet Default AppDomain (faktisk har de mindst tre AppDomains, men to af dem håndteres internt af CLRen, så typisk figurerer disse ikke i diskussionen). Alle .NET-applikationer har ligeledes mindst en applikationstråd. Det vigtige i denne sammenhæng er, at denne ene tråd kommer på grund af processen og ikke på grund af AppDomainet. Der er således ingen sammenhæng mellem AppDomains og tråde, hvilket er en af de væsentlige forskelle mellem processer og AppDomains.

Hvis vi forestiller os en webserver-model, hvor hver kørende applikation er repræsenteret af en selvstændig proces, vil vi hurtig ramme en flaskehals på grund af processers relative høje omkostninger. Hver .NET-proces har som nævnt mindst en applikationstråd. Dertil kommer CLRens interne tråde til garbage collection og afvikling af finalizers. Det vil sige, mindst tre tråde per kørende applikation. Som jeg tidligere har beskrevet, er tråde ikke en billig ressource. Dertil kommer at oprettelse af processer er ret tungt på Windows, så sammenholder man disse detaljer, bliver det hurtig klart, at en model baseret på processer ikke skalerer særlig godt.

AppDomains giver os et brugbart supplement til processer. Fortsætter vi eksemplet, kan en webserver f.eks. have en proces med et antal AppDomains – et per applikation. Hver applikation kan få sin egen tråd, men de kan deles om garbage collection og finalizer-tråd. Det er i sig selv en reduktion, der skærer antallet af tråde ned til lidt over en tredjedel i forhold til modellen med processer. Hvis applikationerne alle er kortlivede, kan de tilmed afvikles af tråde i CLRens thread pool, hvilket gør, at vi kan spare de høje omkostninger ved at oprette og nedlægge tråde hele tiden.

Det er selvfølgelig nærliggende at tro, at hvis vi presser en masse applikationer ind i en proces, vil de så ikke komme til at køre dårligere end det tilsvarende antal selvstændige processer? Der er selvfølgelig nogle mindre konsekvenser af at dele oprydningstråde, men i forhold til tildeling af køretid foregår dette jo som bekendt ikke på procesniveau. Windows tildeler udelukkende køretid til tråde, og uden at skulle gå i detaljer foregår det som følger: så længe der er højprioritetstråde, der er klar til at køre, får de lov at køre, før andre tråde med lavere prioritet kommer til. Så hvis vores server proces afvikles med højere prioritet end konkurrerende applikationer på maskinen, vil alle vores tråde favoriseres i forhold til de andre tråde. Ergo stiller dette ikke vores applikationer dårligere.

AppDomains giver os altså en model, hvor vi kan indlæse og afvikle delapplikationer efterhånden som behovet opstår, uden at det går ud over ydelsen. Når vi ikke længere har brug for en given delapplikation, kan vi give afkald på det relevante AppDomain og dermed slippe af med de assemblies, der er indlæst i dette (dette er lidt forsimplet, men jeg vender tilbage med detaljerne i et senere indlæg).

Så for at afrunde introduktionen til AppDomains: de giver os et værktøj til at oprette selvstændige letvægtsprocesser i en .NET-applikation. Disse ”processer” kan konfigureres, indlæses og afvikles efterbehov, og når vi ikke længere har brug for dem, kan vi skille os af med de relevante assemblies uden problemer. Vi er desværre nødt til at håndtere det nødvendige antal tråde selv (eller kræve at de enkelte delapplikationer selv står for dette), men AppDomains giver os i hvert fald et afskærmet afviklingsmiljø for de enkelte delapplikationer og ydelse samt skalerbarhed er væsentlig bedre end tilfældet er for almindelige processer.

I næste indlæg vil jeg se på, hvordan vi i praksis arbejder med AppDomains i C#, så stay tuned.