ASPNL logo (1 kb)
zaterdag 17 mei 2008




Microsoft MVP

.NET Codewise Community
<< vorige | overzicht | volgende >>

Parallel taken uitvoeren met meerdere AppDomains

Door Michiel van Otegem
1 augustus 2005

Onlangs was ik bezig met een applicatie waarbij meerdere taken tegelijkertijd uitgevoerd moeten worden. In eerste instantie denk je dan aan multi-threading, maar in dit geval was het nodig om met meerdere AppDomains te werken. Voordat ik daar dieper op in ga zal ik eerst nog even een korte introductie geven tot beide begrippen.

Een .NET applicatie wordt uitgevoerd in een AppDomain (Application Domain). Je kunt een AppDomain vergelijken met een proces, maar er zijn wel verschillen. Er kunnen namelijk meerdere AppDomains in een proces zitten. Een AppDomain biedt een logische afscheiding van het geheugen ten opzichte van andere AppDomains. Onder normale omstandigheden kan een AppDomain van een applicatie niet bij de objecten die een andere applicatie in een ander AppDomain gebruikt. Om een lang verhaal kort te maken: een proces is een "fysiek" proces zoals geleverd door het operating systeem, terwijl een AppDomain een logisch proces is dat geleverd wordt door de Common Language Runtime (CLR).

In een (.NET) applicatie (in een AppDomain dus) wordt code uitgevoerd door een thread. Een thread is dus in feite een code pad. Binnen een AppDomain kunnen meerdere threads actief zijn en verschillende paden aflopen. Wanneer die het geval is spreken we van multi-threading. Dit heeft voordelen, omdat verschillende threads tegelijkertijd uitgevoerd kunnen worden. Dat geldt zeker op multi-processor of hyper threaded machines, maar ook in een machine met een enkele processor is er tijdswinst te behalen. Als de ene thread bijvoorbeeld wacht op gegevens van disk kan een andere thread uitgevoerd worden. En zelfs als dat niet het geval is krijgen threads een zogenaamde timeslice toegewezen om te werken. Als deze afgelopen is moet de thread wachten totdat het een volgende timeslice krijgt, zodat andere threads uitgevoerd kunnen worden. Multi-threading is niet alleen handig om de snelheid te verbeteren, maar kan ook voor de gebruiker prettiger zijn. Een langdurige taak voor een Windows applicatie kan op een aparte thread uitgevoerd worden, zodat de gebruikersinterface van een applicatie niet "bevriest".

Het verschil tussen een AppDomain en een thread is dat verschillende threads binnen een AppDomain toegang hebben tot dezelfde objecten. Dat is dus wel even opletten, want als twee threads proberen hetzelfde object wijzigen kan dat vreemde resultaten opleveren. Het is daarom belangrijk om objecten die op die manier gebruikt worden thread safe te maken. Dit doe je door het object tijdelijk te vergrendelen voor anderen, bijvoorbeeld met behulp van de Monitor class. In C# kun je dit heel eenvoudig doen met het lock statement. De C# compiler vervangt dit overigens onder water door code die gebruik maakt van de Monitor class. Hieronder zie je een voorbeeld van het gebruik van het lock statement.

  1: public class ThreadSafeClass
  2: {
  3:    public void ThreadSafeMethod()
  4:    {
  5:       // Dit object vergrendelen
  6:       lock(this)
  7:       {
  8:       // Hier code uitvoeren die thread safe moet zijn
  9:       }
 10:    }
 11: } 

Dat verschillende threads de objecten delen is in veel gevallen niets mis mee, maar in sommige gevallen wel. In een scenario waarbij een generiek mechanisme wordt gebruikt dat wel afzonderlijke delen moet kunnen onderscheiden bijvoorbeeld. Dit is bijvoorbeeld het geval als je gebruik maakt van static classes (Shared in Visual Basic). Stel dat je logging doet met behulp van een static class (of een object factory), dan worden alle log berichten naar hetzelfde doel geschreven. Dit kan een database zijn, de EventLog, of een tekstbestand, en dan is het niet zo'n probleem. Maar wat nu als je per uitgevoerde taak een log email wilt sturen? Zolang er één taak actief is geen probleem: je maakt een string voor de email body en daar voeg je telkens berichten aan toe, en aan het eind verstuur je de email. Als er een tweede taak actief wordt, worden de berichten daarvan in dezelfde email gezet totdat de eerste taak stopt en het log verstuurt. In die email staan alle berichten van beide taken tot op het eind van de eerste taak. Voor de tweede taak wordt nu een nieuwe string gemaakt die alleen de berichten bevat die na het einde van de eerste taak zijn uitgevoerd. Niet zo mooi dus. Nu zijn er manieren om dit tijdens je ontwerp te identificeren en op te vangen, tenzij je gebruik maakt van een third-party component. In mijn geval had ik echter te maken met een bestaande applicatie die aangepast moest worden om meerdere taken tegelijk uit te voeren. Ik stond voor de keuze om m'n hele applicatie overhoop te gooien, of gebruik te maken van de kennis dat verschillende AppDomains hun eigen objecten verzorgen. Met die wetenschap heb ik een manager gebouwd die voor iedere taak een AppDomain creërt en daarin als het ware de oorsponkelijke applicatie in opstart. Om dit netjes te verzorgen maakte ik twee classes: Manager en Processor. Waarbij de Manager de AppDomains verzorgt, en de Processor in een AppDomain gestart wordt om de taken uit te voeren. Voor sommige zaken was er communicatie nodig van de Processor naar de Manager, en daarvoor heeft de Processor een property die terugwijst naar de Manager. De code voor de Manager en de Processor zie je hieronder.

Manager.cs
  1: using System;
  2: using System.Runtime.Remoting.Lifetime;
  3:  
  4: namespace InProcessLifetimeTest
  5: {
  6:    public class Manager : MarshalByRefObject
  7:    {
  8:       public void DoSomething()
  9:       {
 10:          Console.WriteLine("Manager.DoSomething()");
 11:       }
 12:  
 13:       public void Start()
 14:       {
 15:          AppDomain processorDomain =
     AppDomain.CreateDomain("Processor Domain");
 16:          
 17:          Processor processor;
 18:          processor = (Processor)processorDomain.CreateInstanceAndUnwrap(
     "InProcessLifetimeTest",
     "InProcessLifetimeTest.Processor");
 19:  
 20:          processor.Manager = this;
 21:  
 22:          Console.WriteLine("Manager: starting processor.");
 23:  
 24:          processor.DoWork();
 25:  
 26:          Console.WriteLine("Manager: processor done.");
 27:       }      
 28:    }
 29: }


Processor.cs
  1: using System;
  2: using System.Runtime.Remoting.Lifetime;
  3: using System.Threading;
  4:  
  5: namespace InProcessLifetimeTest
  6: {
  7:    public class Processor : MarshalByRefObject
  8:    {
  9:       private Manager m_Manager;
 10:  
 11:       public Manager Manager
 12:       {
 13:          get
 14:          {
 15:             return m_Manager;
 16:          }
 17:          set
 18:          {
 19:             m_Manager = value;
 20:          }
 21:       }
 22:  
 23:       public void DoWork()
 24:       {
 25:          Console.WriteLine(string.Format(
     "Processor: DoWork in AppDomain '{0}'",
     AppDomain.CurrentDomain.FriendlyName));
 26:          
 27:          for(int i = 0; i < 6; i++)
 28:          {
 29:       // Minuut wachten en dan info opvragen
 30:             Thread.Sleep(60000);
 31:             this.WriteLeaseInfo();>
 32:          }
 33:  
 34:          try
 35:          {
 36:       // Communicatie met de manager in ander AppDomain
 37:             this.Manager.DoSomething();
 38:          }
 39:          catch(Exception ex)
 40:          {
 41:             Console.WriteLine(string.Format(
     "Processor: Error on call back to manager. Error message: {0}",
     ex.Message));
 42:          }
 43:       }
 44:  
 45:       private void WriteLeaseInfo()
 46:       {
 47:          ILease lease = this.GetLifetimeService() as ILease;
 48:          if(lease == null)
 49:          {
 50:             Console.WriteLine("Processor: No lease information");
 51:             return;
 52:          }
 53:          
 54:          Console.WriteLine(string.Format(
     "Processor: CurrentLeaseTime={0}",
     lease.CurrentLeaseTime));
 55:          Console.WriteLine(string.Format(
     "Processor: CurrentState={0}",
     lease.CurrentState));
 56:       }
 57:    }
 58: }

In de code hierboven zie je dat beide objecten afgeleid zijn van MarshalByRefObject. Dat is nodig omdat er communicatie plaatsvindt tussen AppDomains aan de hand van deze objecten. De referentie naar het object moet dus over de AppDomain boundary heen en daarvoor is marshalling nodig. In eerste instantie dacht ik dat dit genoeg was, maar toen Processor objecten langer dan 5 minuten bezig waren tot er weer interactie tussen Manager en Processor was, kreeg ik de volgende foutmelding:

Object <...> has been disconnected or does not exist at the server.

Dit is een foutmelding die normaal gezien voorkomt bij Remoting, maar ik deed toch helemaal geen Remoting? Of toch wel...? Een van mijn "Bijbels" Programming .NET Components (zie .NET Framework boeken) zegt dat dit niet zou moeten gebeuren, omdat twee AppDomains binnen hetzelfde proces gebruik maken van dezelfde managed heap. Helaas blijkt dat dus niet te kloppen (de rest van het boek verder geweldig, en ik raad dan ook aan om voor .NET 2.0 de 2nd Edition aan te schaffen), want als je de code uitvoert door een Manager object te maken en de Start methode aan te roepen (bijvoorbeeld in een console applicatie) blijkt dat na 5 minuten de referentie tussen de objecten verdwenen is, ondanks dat beide objecten nog wel "in leven" zijn!

Wat is er aan de hand?
Een MarshalByRefObject object heeft standaard een daarmee geassocieerde lifetime van 5 minuten. Als er geen referenties zijn naar het object die in de afgelopen 5 minuten het object aangeroepen hebben, wordt verondersteld dat het object niet meer nodig is, en wordt het vrijgegeven... althans als het om Remoting gaat. In dat geval is het object namelijk "slapende" totdat er weer een aanroep is. In dit geval zijn beide objecten actief, want de Manager wacht op de Processor, terwijl die bezig is. Echter, bij het geven van de referentie naar Manager aan Processor, krijgt de Processor te horen dat de liftime van de Manager 5 minuten is. Er wordt een proxy aangemaakt naar de Manager in het AppDomain waar de Processor werkt, maar na het verlopen van de lifetime wordt het proxy object vrijgegeven en opgeruimd.

Hoe voorkom je dit?
Er is gelukkig een eenvoudige manier om dit te voorkomen. Elk class die erft van MarshalByRefObject heeft een methode InitializeLifetimeServices, die de standaard lifetime (of lease time) retourneert. Je kunt deze methode overschrijven en deze instellingen aanpassen. Als je wilt dat het object altijd beschikbaar blijft totdat jij het opruimt, kun je deze methode null (Nothing in Visual Basic). Laten retourneren. Dat is echter alleen verstandig in dit soort scenario's. Bij echte Remoting scenario's kun je de verbinding niet garanderen, omdat de AppDomains niet binnen één applicatie vallen. Gebruik in dat geval de daarvoor bedoelde instellingen in App.config.


<< vorige | ^ naar boven | overzicht | volgende >>
copyright 2000-2007 ASPNL