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.
