AppDomain–część II

Dzisiaj pokażę, jak od strony programistycznej wygląda AppDomain. Zwykle tworzymy aplikację host, która trzyma referencje do kilku AppDomain. Stwórzmy najpierw aplikację konsolową wyświetlającą po prostu tekst:

namespace ConsoleApplication3
{
    class Program
    {
        static void Main(string[] args)
        {
            while (true)
            {
                Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
                System.Threading.Thread.Sleep(2000);
            }
        }
    }
}

Jeśli uruchomimy aplikację, domyślnie oczywiście będzie działała w osobnym procesie. Z tego względu CurrentDomain.FriendlyName wyświetli ConsoleApplication3.exe – nazwę procesu.

ConsoleApplication3 uruchomimy w osobnym AppDomain. Przejdźmy do aplikacji hostującej, która stworzy AppDomain i załaduje potrzebne zasoby.

Aby stworzyć podstawową domenę, wystarczy wywołać statyczną metodę AppDomain.CreateDomain:

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain appDomain=AppDomain.CreateDomain("Nazwa AppDomain");
            appDomain.ExecuteAssembly(@"ConsoleApplication3.exe");            
        }
    }
}

ExecuteAssembly działa w sposób synchroniczny, czyli blokuje dalsze wywołanie kodu aż do momentu zakończenia ConsoleApplication3. Jeśli zajrzymy w TaskManager, zobaczymy wyłącznie ConsoleApplication2.exe ponieważ ConsoleApplication3.exe został uruchomiony jako AppDomain a nie nowy proces. Jest to oczywiście znaczącą szybsze niż odpalanie nowego procesu a stopień izolacji pozostaje podobny. Po uruchomieniu na ekranie zobaczymy tekst “Nazwa AppDomain” a nie jak wcześniej “ConsoleApplication3.exe”.

ExecuteAssembly ładuje daną bibliotekę i wywołuje entry method. Istnieje również możliwość załadowania biblioteki i wykonanie konkretnej metody na danej klasie. Stwórzmy zatem bibliotekę i jakąś przykładową klasę:

public class PrintHelper : MarshalByRefObject
{
    public void Print()
    {
        Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
    }
}

Pierwsza nowość: dziedziczymy po MarshalByRefObject. Ze względu na fakt, że obiekty w dwóch różnych AppDomain są od siebie odizolowane, nie istnieje bezpośredni sposób komunikacji między nimi. Oczywiście gdybyśmy mogli bezpośrednio odwoływać się do klas w innych AppDomain, byłoby to z sprzeczne z ideą, którą od dwóch postów opisuję – izolacja i bezpieczeństwo. Najpopularniejszym sposobem komunikacji między domenami jest .NET Remoting. Istnieje również możliwość komunikacji za pomocą protokołu named pipes oraz WCF ale to temat na inny post. Komunikujemy się zatem w sposób pośredni. MarshalByRefObject mówi, że dla obiektu zostanie stworzony proxy. Klient zatem będzie korzystał nie z PrintHelper a z proxy, który wywołuje metody na zadanej AppDomain. W świecie .NET remoting ma to taką wadę, że w przypadku wielu wywołań za każdym razem musimy wysyłać żądanie obciążając tym samym sieć. Drugim sposobem jest marshal by value czyli przekazanie kopii obiektu do klienta i wykonanie zadanej logiki po stronie klienta. Jeśli chcemy korzystać z takiego podejścia nie musimy dziedziczyć po żadnej klasie – wystarczy doczepić atrybut Serializable. Decyzja zależy od scenariusza – jeśli nie mamy dużą wywołań lepiej skorzystać z MarshalByRefObject. Z kolei dla małych obiektów z wysoką liczbą wywołań wydajniejszym rozwiązaniem jest serializacja\deserializacja (marshal by value).

Wiemy już, że do komunikacji między AppDomain musimy skorzystać ze specjalnych mechanizmów. Przejdźmy do aplikacji hostującej, która chce wywołać PrintHelper w innej domenie:

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain appDomain = AppDomain.CreateDomain("Nazwa AppDomain");

            var domainHelper =
                (PrintHelper)
                appDomain.CreateInstanceAndUnwrap(typeof (PrintHelper).Assembly.FullName, typeof(PrintHelper).FullName);
            domainHelper.Print();

            PrintHelper processHelper = new PrintHelper();
            processHelper.Print();
        }
    }
}

CreateInstanceAndUnwrap służy do stworzenia oraz wykonania unwrap (o tym później) na obiekcie – na skutek czego dostaniemy po prostu instancję obiektu (w tym przypadku proxy). domainHelper wykonuje kod w innej domenie i wywołanie metody Print wydrukuje “Nazwa AppDomain”. Z kolei processHelper to zwykła instancja i wywołanie Print zwróci nazwę aktualnego procesu (ConsoleApplication2). Aby przekonać się, że domainHelper to faktycznie proxy wystarczy np. zajrzeć do debuggera a ujrzymy jako typ “{System.Runtime.Remoting.Proxies.__TransparentProxy}”. Po usunięciu MarshalByReflectObject i dodaniu atrybutu Serializable ujrzymy prawdziwą nazwę obiektu ponieważ został on całkowicie skopiowany i nie ma potrzeby używania proxy. Ale zastanówmy się co właśnie zrobiliśmy…Nakazaliśmy skopiować cały obiekt i przesłać go do klienta. A co z izolacją? Niestety to co zrobiliśmy było głupie ponieważ kod zostanie wykonany na ConsoleApplication2.exe a nie na domenie “Nazwa AppDomain”.  Marshal by value lepiej pozostawić dla kontenerów służących np. jako parametry. W pozostałych przypadkach korzystajmy z MarshalByRefObject (jak w powyższym kodzie).

Następna kwestia to operacja Unwrap. Aby wyjaśnić, przyjrzyjmy się drugiej metodzie do tworzenia obiektów CreateInstance:

ObjectHandle handler=appDomain.CreateInstance(typeof (PrintHelper).Assembly.FullName, typeof(PrintHelper).FullName);

var pritnHelper = (PrintHelper) handler.Unwrap();

CreateInstance zawraca ObjectHandler, który jest wrapperem naszego obiektu. Aby dostać się do prawdziwego obiektu wystarczy wywołać UnWrap. Wcześniej użyta metoda CreateInstanceAndUnwrap wykonuje te dwie operacje od razu. Podstawowe pytanie jednak brzmi: po co tak kombinować z tym wrapperem? Nie można byłoby zawsze zwracać właściwy obiekt? Można byłoby, ale nie zawsze jest to wydajne. W końcu musimy albo stworzyć proxy lub wykonać dokładną kopię obiektu. Są to operacje dość czasochłonne a nie zawsze chcemy je od razu wykonywać. W końcu możemy stworzyć ObjectHandle na początku, potem go przekazywać przez jakieś parametry metod a dopiero na końcu wykonać metodę.

Jak już wspomniałem  w poprzednim poście, bibliotek w środowisku .NET nie da się usunąć z pamięci. Możliwe jednak jest usunięcie AppDomain wraz z załadowanymi w niej bibliotekami. Z tego względu metoda AppDomain.Unload jest bardzo ważna z punktu widzenia zużycia pamięci:

AppDomain.Unload(appDomain);

Na zakończenie warto dodać, że to tylko początek (jak to zwykle bywa). Często tworzy się AppDomain aby wykonać jakiś kod z użyciem innym przywilejów.  W momencie tworzenia nowego AppDomain można przekazać dodatkowe, konfiguracyjne  parametry:

AppDomainSetup ads = new AppDomainSetup();
ads.ApplicationBase = System.Environment.CurrentDirectory;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;

Evidence baseEvidence = AppDomain.CurrentDomain.Evidence;
Evidence evidence = new Evidence(baseEvidence);

AppDomain ad2 = AppDomain.CreateDomain(name, evidence, ads);

One thought on “AppDomain–część II”

Leave a Reply

Your email address will not be published.