niedziela, 18 kwietnia 2010

Programowanie wielowątkowe w C# cz. 1

Trudno sobie wyobrazić współcześnie pisaną aplikację pozbawioną wątków. W erze, kiedy producenci procesorów dodają kolejne rdzenie do swoich najnowszych modeli, stosowanie podziału obciążenia obliczeniowego na kilka jednostek jest nieuniknione.
Naukę o wątkach należałoby rozpocząć od terminu procesu, który to jak mówi Wikipedia: „jedno z najbardziej podstawowych pojęć w informatyce, definiowane jako egzemplarz wykonywalnego programu”. Tak więc z każdą uruchomioną w naszym komputerze aplikacją możemy skojarzyć przynajmniej jeden proces. Oczywiście nic nie stoi na przeszkodzie, aby aplikacja posiadała więcej niż jeden proces. Zarządzaniem procesami zajmuje się System Operacyjny. Każdy utworzony proces otrzymuje od Systemu operacyjnego czas procesora, pamięć, dostęp do urządzeń I/O oraz dostęp do plików. Dostęp do uruchomionych procesów w systemie Windows uzyskujemy w Menedżerze zadań:


Może nasunąć się pytanie: „Po co więc używać wątków skoro mamy procesy?”. Otóż jeśli spojrzymy na poniższe diagramy z pewnością uchwycimy to, co czyni wątki wydajniejszymi:


Na pierwszym rysunku mamy klasyczny przykład procesu pozbawionego wątków. Drugi zawiera już utworzone dwa wątki. Przejdźmy do odpowiedzi na postawione pytanie. W pierwszym przypadku, aby zwiększyć szybkość wykonywania programu, można by utworzyć kolejny proces. W drugim przypadku tworzymy kolejny wątek w bieżącym procesie. Jak łatwo można zauważyć dzięki wątkom oszczędzamy na czasie i ułatwiamy sobie pracę. Tworząc wątek, posiadamy możliwość korzystania z współdzielonej przestrzeni adresowej, co pozwala na łatwą komunikację między wątkową bez zbędnych potrzeb korzystania z funkcji systemu operacyjnego. Także tworzenie wątków jest dużo szybsze i wymaga mniejszych zasobów. Podsumowując, na korzyść wątków przemawiają szybkość tworzenia, mniejsze użycie zasobów oraz łatwa komunikacja pomiędzy nimi.
Należy zapamiętać, że podczas wykonywania się wątków w systemie operacyjnym, wykonuje się tylko jeden na danym procesorze. Aby podglądnąć ilość wątków w naszych aplikacjach można do menedżera zadań dodać kolumnę wątki:


Jak widać, każdy proces posiada mniej lub więcej uruchomionych wątków.
Proces może mieć kilka stanów:
Nowy – proces został utworzony, otrzymał zasoby oprócz czasu procesora
Gotowy – proces oczekuje na przydział kwantu czasu procesora
Oczekujący – czasami też nazywany uśpiony, proces jest zatrzymany w wyniku niedomiaru zasobów. Stan ten może być też spowodowany oczekiwaniem na odpowiedź od urządzenia I/O
Wykonywany – instrukcje procesu są wykonywane
Zakończony – proces zakończył swoje działanie, oddaje zarezerwowane zasoby; w razie potrzeby informuje inne procesy o zakończeniu swojego działania.

Zarządzaniem procesami, czyli między innymi kiedy któremu przyznać kwant czasu procesora zajmuje się system operacyjny. Dzięki sprawnemu przełączeniu pomiędzy wykonywanymi procesami na maszynach jednoprocesorowych, gdzie może być wykonywany tylko jeden proces jednocześnie, użytkownik nie odczuwa dyskomfortu gdy pracuje z wieloma aplikacjami jednocześnie.

Każdy nowoutworzony wątek w systemie operacyjnym może otrzymać priorytet. Platforma .NET jak i system Windows pozwala nadać następujące priorytety (zaczynając od najwyższego): Highest, AboveNormal, Normal, BelowNormal, Lowest. Należy sobie oczywiście zdać sprawę z tego, że decydującą rolę o ważności wątku ma system operacyjny a nie użytkownik.

Tworzenie wątków na platformie .NET jest niezwykle proste. Aby utworzyć nowy wątek należy utworzyć instancję klasy Thread, korzystając z jednego z dostępnych konstruktorów:
    1         //korzystając z ThreadStart
    2         private static void Method1()
    3         {
    4             for (int i = 0; i < 5; i++)
    5             {
    6                 Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    7             }
    8         }
    9 
   10         //korzystając z ParameterizedThreadStart
   11         private static void Method2(object o)
   12         {
   13             for (int i = 0; i < 5; i++)
   14             {
   15                 Console.WriteLine(o.ToString());
   16             }
   17         }
   18 
   19         static void Main(string[] args)
   20         {
   21             Thread t1 = new Thread(Method1);
   22             Thread t2 = new Thread(Method2);
   23             t1.Start();
   24             t2.Start("Dane dla wątku");
   25         }

Jak widać dane dla wątku są przekazywane w metodzie Start(). Przyjmuje ona zawsze parametr typu object, dlatego też sami musimy zatroszczyć się o to jak będziemy rzutowali dane w kodzie dla wątku. Oczywiście można skorzystać tutaj z różnych dodatków C# takich jak np. wyrażenia lambda czy inne metody tworzenia kodu dla wątku:

            Thread t1 = new Thread(delegate()
            {
                for (int i = 0; i < 5; i++)
                {
                    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                }
            });
            Thread t2 = new Thread(delegate(object o)
                {
                    for (int i = 0; i < 5; i++)
                    {
                        Console.WriteLine(o.ToString());
                    }
                });

            //Lambda expressions
            Thread t3 = new Thread(() =>
                {
                    for (int i = 0; i < 5; i++)
                    {
                        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                    }
                });

            Thread t4 = new Thread((o) =>
                {
                    for (int i = 0; i < 5; i++)
                    {
                        Console.WriteLine(o.ToString());
                    }
                });

Podczas swojego działania wątki mogą przyjąć różne stany w zależności czy aktualnie się wykonują, są zawieszone czy też wstrzymane. W .NET Framework mamy następujące stany do wykorzystania:
Running - wątek jest wykonywany
StopRequested - prośba o wstrzymanie wątku - tylko do wewnętrznego użytku
SuspendRequested - prośba o zawieszenie wątku
Background - wątek jest wykonywany w tle, uzyskujemy to poprzez ustawienie właściwości Thread.IsBackground na true.
Unstarted - nie wykonano na wątku metody Start.
Stopped - wątek został zatrzymany.
WaitSleepJoin - wątek jest zablokowany, najczęściej jest to spowodowane użyciem metody Sleep lub Join; może też to być spowodowanem zablokowaniem dostępu do danych (lock)
Suspended - wątek został zawieszony
AbortRequested - została wywołana metoda Abort na wątku, jednak jeszcze nie został zakończony
Aborted - wątek został usunięty, jednak stan jeszcze nie został przełączony na Stopped

Metoda Join w kontekście wykorzystania wątków ma szczególne zastosowanie. Dzięki niej, jeśli wykonujące wątki operują na danych które przykładowo modyfikują, należy zagwarantować, że wątki wzajemnie na siebie zaczekają. W innym przypadku operacje były by nie tyle niebezpieczne co bezsensowne. Przyjrzyjmy się następującemu kodu:

    class MyClass
    {
        public int MyProperty { get; set; }
        public MyClass()
        {
            this.MyProperty = 0;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyClass myClass = new MyClass();

            Thread t1 = new Thread((o) =>
                {
                    myClass.MyProperty *= (int)o;
                });

            Thread t2 = new Thread((o) =>
                {
                    myClass.MyProperty += (int)o;
                });

            object variable = 10;
            t1.Start(variable);
            t2.Start(variable);
            Console.WriteLine(myClass.MyProperty);
        }
    }

Mamy tutaj do czynienia z klasą posiadającą jedno pole – właściwość typu integer. Po utworzeniu obiektu klasy nadajemy temu polu wartość 0 (właściwie nie musielibyśmy tego robić, gdyż podczas tworzenia obiektu jego pola otrzymują domyślne wartości a dla integera jest to i tak 0). Teraz w metodzie Main tworzymy dwa wątki, korzystając przy tym z wyrażeń lambda. Pierwszy wątek mnoży to co dostanie na wejściu przez aktualną wartość pola, drugi dodaje jakąś wartość do wartości pola. Jeśli uruchomimy ten program kilka razy, może okazać się że za każdym razem otrzymujemy inny wynik. Możemy otrzymać 0, 10 lub 100. Uruchamiając ten program tak naprawdę uruchamiamy nie 2 wątki a co najmniej 3. Ten trzeci wątek – który nie widzimy tutaj tak wprost to nasza aplikacja. Kod który jest wykonywany w metodzie Main działa na oddzielnym wątku. Pozostałe dwa wątki tworzymy już jawnie. W tym wypadku pomiędzy wątkami nie ma żadnej synchronizacji. Ktoś by powiedział: wrzucić metodę Sleep w każdym wątku o odpowiednim czasie i po problemie. Jest to pomysł, jednak niezwykle mało elastyczny. Naszym zamierzeniem jest najpierw dodanie do właściwości wartości znajdującej się w zmiennej variable, a następnie wymnożenie tej wartości przez wartość zmiennej variable. Tutaj właśnie przychodzi nam z pomocą metoda Join. Spójrzmy na nasz kod po dodaniu tej metody:

    class MyClass
    {
        public int MyProperty { get; set; }
        public MyClass()
        {
            this.MyProperty = 0;
        }
    }

    class Program
    {
        private static Thread t1;
        private static Thread t2;
        static void Main(string[] args)
        {
            MyClass myClass = new MyClass();
            t1 = new Thread((o) =>
                {
                    myClass.MyProperty *= (int)o;

                });

            t2 = new Thread((o) =>
                {
                    myClass.MyProperty += (int)o;
                });

            object variable = 10;
            t2.Start(variable);
            t2.Join();
            t1.Start(variable);
            t1.Join();
            Console.WriteLine(myClass.MyProperty);
        }
    }

Dzięki funkcji Join zawsze uzyskamy oczekiwany wynik.

W przypadku wątków mamy możliwość zastosowania także innych funkcji jak: Abort (przerywa działanie wątku), Resume (wznawia pracę wątku), Suspend (zawiesza wątek).
Korzystanie z tych metod powinno być decyzją przemyślaną. Dzieje się tak, gdyż nigdy nie mamy pewności, że zawieszony wątek uda nam się wznowić. Microsoft także zaleca używanie innych rozwiązań w pracy z wątkami jak Mutexy, Semafory czy też monitory.

Synchronizacja wątków nigdy nie była sprawą prostą i oczywistą. Wyobraźmy sobie sytuację, w której mamy pobrać dane, wykonać na nich operacje a następnie zapisać do pliku/bazy danych. Operacja może wydawać się bardzo prosta. Dla potrzeb tego artykułu przyjmijmy, że wszystkie te operacje zabierają więcej niż 2 sekundy. Możemy teraz wyobrazić sobie użytkownika, który obsługuje naszą aplikację. Kiedy rozpoczną się wymienione operacje interfejs użytkownika zamiera. Jeżeli użytkownik nie ma możliwości interakcji z naszą aplikacją, pierwszą myślą jaka mu przychodzi do głowy jest zabicie procesu (naszej aplikacji). Teraz weźmy tą samą aplikację tylko podzieloną na wątki. Dzięki temu aplikacja podczas wykonywania operacji nie zamrozi całego interfejsu. Problemem jednak jest tutaj synchronizacja między wątkami. Czynność, którą nazwaliśmy wykonanie operacji, musi wykonać się po pobraniu danych, a zapisanie do pliku/bazy danych po wykonaniu operacji.
Jak już wspominałem wcześniej, w .NET istnieje kilka klas za pomocą których możemy uzyskać synchronizację naszych wątków. Przyjrzyjmy się ich hierarchii:

System.Threading.WaitHandle
• System.Threading.EventWaitHandle
• System.Threading.Mutex
• System.Threading.Semaphore

Klasa WaitHandle jest klasą bazową dla trzech pozostałych klas. Należy tutaj zaznaczyć, że klasa ta implementuje interfejs IDisposable, co sugeruje nam, że po skończeniu jej użytkowania należy zwolnić zasoby zarezerwowane przez nią. Świetne wyobrażenie o synchronizacji wątków przedstawił Sacha Barber w swoim artykule (link można znaleźć w źródłach):
Przedstawił on synchronizację jako przejazd kolejowy. Przed przejazdem czeka kolejka samochodów (wątków). Zapora podnosi się w zależności od potrzeb i pozwala na przejazd tylko jednego samochodu.

        static void Main(string[] args)
        {

            Thread pobranie = new Thread(() =>
                {
                    for (int i = 0; i < 5; i++)
                    {
                        Thread.Sleep(500);
                        Console.WriteLine("Pobieranie danych");
                    }
                });

            Thread operacje = new Thread(() =>
                {
                    for (int i = 0; i < 4; i++)
                    {
                        Thread.Sleep(250);
                        Console.WriteLine("Przetwarzanie danych");
                    }
                });

            Thread zapis = new Thread(() =>
                {
                    for (int i = 0; i < 8; i++)
                    {
                        Thread.Sleep(150);
                        Console.WriteLine("Zapis danych");
                    }
                });
            pobranie.Start();
            operacje.Start();
            zapis.Start();
        }

Jeśli uruchomimy powyższy kod otrzymamy taki sam (bądź bardzo podobny) wynik:


Z pewnością taki wynik nas nie zadawala, a co więcej w realnej aplikacji może zagrozić danym wprowadzonym przez użytkownika (o ile w ogóle zadziała). Spróbujmy skorzystać z wymienionych poprzednio klas do uzyskania synchronicznych wywołań. Na początek może kod i zrzut ekranu aplikacji po zastosowaniu tej klasy, a później parę słów komentarza:

    class Program
    {
        private static EventWaitHandle ewhOperacje = new AutoResetEvent(false); // 0.
        private static EventWaitHandle ewhZapis = new AutoResetEvent(false); // 0.

        static void Main(string[] args)
        {

            Thread pobranie = new Thread(() =>
                {
                    for (int i = 0; i < 5; i++)
                    {
                        Thread.Sleep(500);
                        Console.WriteLine("Pobieranie danych");
                    }
                    ewhOperacje.Set(); //1.
                });

            Thread operacje = new Thread(() =>
                {
                    ewhOperacje.WaitOne(); //2.
                    for (int i = 0; i < 4; i++)
                    {
                        Thread.Sleep(250);
                        Console.WriteLine("Przetwarzanie danych");
                    }
                    ewhZapis.Set(); // 1.
                });

            Thread zapis = new Thread(() =>
                {
                    ewhZapis.WaitOne(); // 2.
                    for (int i = 0; i < 8; i++)
                    {
                        Thread.Sleep(150);
                        Console.WriteLine("Zapis danych");
                    }
                });
            pobranie.Start();
            operacje.Start();
            zapis.Start();
        }
    }


W porównaniu do poprzedniego przykładu, ten kod działa tak jakbyśmy tego chcieli. Przejdźmy do objaśnienia zaznaczonych fragmentów kodu:
0 – Jest to utworzenie obiektów klasy AutoResetEvent. Klasa ta umożliwia wątkom komunikowanie się między sobą poprzez wysyłanie sygnałów. Wątek oczekuje na sygnał wywołując metodę WaitOne – 2na obiekcie klasy AutoResetEvent. Jeżeli obiekt jest w stanie nie sygnalizowanym (false), wtedy wątek jest blokowany, aż do momentu kiedy blokujący wątek wywoła metodę Set – 1. Klasa AutoResetEvent posiada tą szczególną właściwość, że po wywołaniu metody Set i odblokowaniu wątku, od razu przechodzi w stan braku sygnału (false).
Istnieje także klasa ManualResetEvent, która pozwala uzyskać takie samo zachowanie jak AutoResetEvent, ale także umożliwia „manualne” zmienianie sygnału. Po wywołaniu Set i zmianie sygnału nie zostanie on zmieniony na brak sygnału automatycznie. W zależności od potrzeb można korzystać z jednej lub drugiej wersji klasy.

Przejdźmy teraz do Mutexów. Mutex w swoim działaniu jest bardzo podobny do lock (o nim trochę później w tym artykule). W odróżnieniu jednak od locka pozwala na działanie w obrębie „komputera” a nie tylko aplikacji. Należy też zaznaczyć, że lock jest dużo szybszy od Mutexu. Jest jednak jedno szczególne zastosowanie Mutexa, które znalazło szerokie zastosowanie w programowaniu. Dzięki temu, że Mutex nie jest zależny tylko od aplikacji, można dzięki niemu zaimplementować możliwość uruchomienia tylko jednej instancji naszej aplikacji. Zobaczmy na przykład:
    class Program
    {
        private static Mutex mutex = new Mutex(false, "test");
        static void Main(string[] args)
        {
            if (!mutex.WaitOne(1000))
            {
                Console.WriteLine("Mutex został zainicjowany! Wychodziny");
                return;
            }
            try
            {
                Console.WriteLine("Stworzono Mutex");
                Console.ReadLine();
            }
            finally
            {
                mutex.ReleaseMutex();
            }
        }
    }

Po uruchomieniu 2 instancji naszej aplikacji zobaczymy:

    class Program
    {
        private static Mutex mutex = new Mutex(false, "test");
        static void Main(string[] args)
        {
            if (!mutex.WaitOne(1000))
            {
                Console.WriteLine("Mutex został zainicjowany! Wychodziny");
                return;
            }
            try
            {
                Console.WriteLine("Stworzono Mutex");
                Console.ReadLine();
            }
            finally
            {
                mutex.ReleaseMutex();
            }
        }
    }

Po uruchomieniu 2 instancji naszej aplikacji zobaczymy:


Ważne jest tutaj wspomnieć, że w przypadku awarii naszej aplikacji Mutex zostanie automatycznie zwolniony.

Przejdźmy teraz do Semaforów. W Internecie można znaleźć takie porównanie: „Semafor jest jak nocny klub – ma określoną pojemność pilnowaną przez bramkarza. Kiedy klub jest pełny, nikt więcej nie wejdzie dopóki ktoś nie wyjdzie. Podczas tworzenia obiektu tej klasy, konstruktor wymaga dwóch parametrów – pojemności klubu oraz ilość wolnych miejsc.
Spójrzmy na następujący kod:

    class Program
    {
        private static Semaphore s = new Semaphore(2, 2);
        static void Main(string[] args)
        {
            for (int i = 0; i < 10; i++)
            {
                Thread t = new Thread(Run);
                t.Name = i.ToString();
                t.Start();
            }
        }

        public static void Run()
        {
            s.WaitOne();
            Console.WriteLine(Thread.CurrentThread.Name);
            Thread.Sleep(1000);
            s.Release();
        }
    }

Tworzymy tutaj obiekt klasy Semaphore o pojemności 2 wątków i z wolnymi dwoma miejscami na wątki. Następnie tworzymy 10 wątków i przekazujemy im do wykonania metodę Run. Metoda ta może dzięki Semaphorze być obsługiwana tylko przez dwa wątki na raz. Gdzie może się nam coś takiego przydać w praktyce? Przekładowo do zarządzania połączeniami do bazy danych. Możemy z góry ustalić że będzie maksymalnie 10 połączeń a Semaphora zajmie się zarządzaniem ich.

Lock
Przy omawianiu Mutexów wspomniałem o locku. Lock podobnie jak Mutex pozwala na ochronę sekcji kodu, aby tylko jeden wątek mógł mieć do niego dostęp. Jak wcześniej wspominałem lock jest znacznie szybszy od mutexa i zaleca się jego stosowanie.
Może się tutaj nasunąć pytanie: po co stosować lock? Otóż spójrzmy na poniższy kod, który chyba najlepiej obrazuje zastosowanie locka:

    class Program
    {
        private static int a = 62;
        private static int b = 20;

        static void Main(string[] args)
        {
            for (int i = 0; i < 100; i++)
            {
                new Thread(Divide).Start();
            }
        }

        private static void Divide()
        {
            b = 23;
            if (b != 0)
            {
                Console.WriteLine(a / b);
            }
            b = 0;
        }
    }

Uruchamiając powyższy kod raz, dwa, sto razy możemy odnieść wrażenie, że wszystko działa poprawnie. Jednak tak nie jest. Praca z wątkami często prowadzi do pułapek. Kod który podano powyżej nie jest bezpieczny. Można sobie wyobrazić sytuację, kiedy jeden wątek jest w momencie przypisywania do zmiennej b wartości 0 a drugi wątek jest w momencie wypisywania na konsoli wartości wyrażenia. Aby zapobiec takim zdarzeniom, należy zablokować możliwość zmiany wartości krytycznej sekcji naszego programu.
Poprawimy kod korzystając z lock:

    class Program
    {
        private static int a = 62;
        private static int b = 20;
        private static object o = new object();

        static void Main(string[] args)
        {
            for (int i = 0; i < 100; i++)
            {
                new Thread(Divide).Start();
            }
        }

        private static void Divide()
        {
            lock (o)
            {
                b = 23;
                if (b != 0)
                {
                    Console.WriteLine(a / b);
                }
                b = 0;
            }
        }
    }

Do kodu wprowadziliśmy dwie konstrukcje. Po pierwsze do klasy dołożyliśmy pole o typie object, a dwa zastosowaliśmy blok kodu lock. Blokowanie musi odbyć się na typie referencyjnym. Często zdarza się, że programiści stosują tutaj konstrukcję lock(this) czy też lock(typof(MyClass)). Należy unikać takich konstrukcji. Dobre nawyki programowania dla lock mówią, że obiekt blokowany powinien być prywatnym polem klasy typu referencyjnego.

ThreadPool
Pula wątków umożliwia w elegancki sposób zarządzać wieloma małymi zadaniami. Tworzenie obiektów Thread powoduje zużycie zasobów, jednak daje możliwość zarządzania wątkiem (wstrzymywanie, wznawianie, przerwanie). ThreadPool zmniejsza zużycie zasobów, jednak po uruchomieniu wątku nie możemy nim jakkolwiek zarządzać. Innym problemem jest to, że wątki w ThreadPool są uruchomiane jako wątki w tle. Dla nas oznacza to, że w momencie zakończenia głównego wątka, niewykonane wątki z puli zostaną przerwane.
Zobaczmy na przykład:

    class Program
    {
        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(Method);
            Console.ReadLine();
        }

        private static void Method(object o)
        {
            Console.Write("Hello from ThreadPool");
        }
    }

Tyle o podstawach wielowątkowości w .NET. W następnej części przyjrzymy się w jaki sposób obchodzić się z wątkami w WindowsForms oraz WPF. W ostatniej części zobaczymy nowości, które wprowadzono do .NET 4.0.

Źródło:
http://aragorn.pb.bialystok.pl/~wkwedlo/OS1-3.pdf
http://msdn.microsoft.com/en-us/library/ms686749%28v=VS.85%29.aspx
http://en.wikipedia.org/wiki/Process_%28computing%29
http://www.codeproject.com/KB/threads/ThreadingDotNet.aspx
http://wazniak.mimuw.edu.pl/images/5/54/Sop_02_wyk_bw_1.1.pdf
http://www.albahari.com/threading/part2.aspx

niedziela, 11 kwietnia 2010

Reflection w .NET

Reflection w .NET daje nam możliwości otwierania, uruchamiania czy też tworzenia dynamicznie typów podczas działania programu. Gdzie się może nam taka funkcjonalność przydać? Przykładowo w aplikacji do której chcemy mieć możliwość dodawania wtyczek.

Ładowanie Assemblies podczas działania programu.
Aby załadować jakieś assembly można skorzystać z kilku dostępnych metod (przed przystąpieniem do tego dodajemy przestrzeń nazw System.Reflection):
Assembly.Load – ładuje assembly na podstawie nazwy (zwykle z GAC)
Assembly.LoadFile – ładuje assembly z pliku na podstawie podanej ścieżki
Assembly.LoadFrom – ładuje assembly na podstawie nazwy pliku lub ścieżki
Assembly.ReflectionOnlyLoad – w większości przypadków ładuje pakiet z GAC w trybie reflection - only
Assembly.ReflectionOnlyLoadFrom – ładuje assembly w trybie reflection – only z podanej ścieżki

Jeżeli skorzystamy z metody, która zawiera tryb relection – only, nie będziemy mieć możliwości tworzenia obiektów ładowanego pakietu, a jedynie możliwość przeglądania właściwości czy też kodu tych pakietów. Może się to przydać kiedy chcemy tylko zobaczyć jak zostały zdefiniowane funkcje interesującego nas assemby.

Przykład załadowania assembly z pliku. Posłużę się tutaj bardzo dobrze opisanym w Training Kit StringBuilderem.
1. Ładujemy assembly z pliku
2. Za pomocą metody GetType wyodrębniamy interesujący nas typ
3. Za pomocą metody GetConstructor pobieramy konstruktor
4. Używając metody GetMethod wyodrębnimy interesującą nas metodę
5. Za pomocą Invoke() uruchamiamy (jak i również przekazujemy jeśli są potrzebne parametry) metodę
Kod:

            //StringBuilder znajduje się w pliku mscorlib.dll
            Assembly a = Assembly.Load("mscorlib.dll");
            //Pobieramy interesujący nas typ
            Type t = a.GetType("System.Text.StringBuilder");
            //Wyodrębniamy konstruktor
            ConstructorInfo ci = t.GetConstructor(new Type[] { });
            //Invoke wywoła konstruktor - w tym przypadku jest bezparametrowy więc
            //można przesłać albo null albo pustą tablicę
            object sb = ci.Invoke(null);
            //Wyodrębniamy potrzebną nam metodę
            MethodInfo mi = t.GetMethod("Append", new Type[] { typeof(string) });
            //Wywołujemy metodę wraz z parametrem
            mi.Invoke(sb, new object[] { "Ala ma kota" });
            //Wypisujemy zawartość StringBuildera na ekranie
            Console.WriteLine(sb);

Kod można uprościć w tym przypadku. Można od razu podstawić żądany typ:

            Type t = typeof(StringBuilder);

Aby otrzymać listę wszystkich metod, właściwości czy konstruktorów można posłużyć się jedną z metod GetMethods, GetProperties, GetConstructors itd. Dla przykładu wyświetlimy wszystkie właściwości StringBuildera i ich typy:

            PropertyInfo[] propinfo = t.GetProperties();
            foreach (var item in propinfo)
            {
                Console.Write(item.Name);
                Type types = item.PropertyType;
                Console.Write("  " + types.Name);
                Console.WriteLine();
            }

Aby wyświetlić wszystkie metody, właściwości, pola, eventy itp. można skorzystać z poniższego kodu:

            MemberInfo[] allTypes = t.GetMembers();
            foreach (var item in allTypes)
            {
                Console.WriteLine(item.Name);
            }

Za pomocą BindingFlags można dokonać slekcji (same metody, właściwości itp.):

            MemberInfo[] allTypes = t.GetMembers(BindingFlags.GetField | BindingFlags.CreateInstance);

Atrybuty Assembly
Atybuty mogą opisywać różne aspekty pakietu. Dla przykładu może to być wersja, nazwa, rok, opis itp. Dla przykładu wyświetlimy prawa autorskie obecnie wykonywanego assembly:

            Assembly a = Assembly.GetExecutingAssembly();
            foreach (Attribute item in a.GetCustomAttributes(false))
            {
                if (item.GetType() == typeof(AssemblyCopyrightAttribute))
                {
                    Console.WriteLine(((AssemblyCopyrightAttribute)item).Copyright);
                }
            }


Dynamiczne generowanie typów
Korzystając z możliwości Reflection możemy w czasie działania programu stworzyć nasz własny typ dzięki klasą w przestrzeni System.Reflection. Możemy w tym celu skorzystać z klas takich jak: AssemblyBuilder, ConstructorBuilder, EnumBuilder, EventBuilder, FieldBuilder, LocalBuilder, MethodBuilder, ModuleBuilder, ParameterBuilder, PropertyBuilder, TypeBuilder (dostęp do typów otrzymujemy po dodaniu przestrzeni nazw System.Reflection.Emit).

Proces tworzenia dynamicznego typu można przedstawić jako następująca lista czynności:
1. Tworzymy assembly i moduł.
2. W module tworzymy żądany typ.
3. Dla typu definiujemy żądane przez nas elementy jak konstruktowy, właściwości, metody itd.
4. Dla każdej metody tworzymy ILGenerator który pozwala nam zapisać kod dla metody

Utworzenie obiektu i wywołanie metody odbywa się jak to opisano w pierwszej części artykułu.
Kod:

            AssemblyBuilder a = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("test"), AssemblyBuilderAccess.RunAndSave);
            ModuleBuilder mb = a.DefineDynamicModule("module");
            TypeBuilder tb = mb.DefineType("MyClass", TypeAttributes.Class | TypeAttributes.Public);
            ConstructorBuilder cb = tb.DefineDefaultConstructor(MethodAttributes.Public);
            MethodBuilder methb = tb.DefineMethod("SaySomething", MethodAttributes.Public, typeof(void), null);
            ILGenerator genCode = methb.GetILGenerator();
            genCode.EmitWriteLine("Hello");
            genCode.Emit(OpCodes.Ret);


            Type t = tb.CreateType();
            ConstructorInfo ci = t.GetConstructor(new Type[] { });
            object o = ci.Invoke(null);
            MethodInfo mi = t.GetMethod("SaySomething");
            mi.Invoke(o, new object[] { });

Podsumowując dzięki Refleksji jesteśmy w stanie tworzyć w łatwy sposób rozszerzalne aplikacje (wtyczki, pluginy itp.). Oprócz tego jesteśmy także w stanie tworzyć własne typy dynamicznie.

wtorek, 6 kwietnia 2010

Stronicowanie w DataGrid

Korzystanie z kontrolki DataGrid jest niezwykle proste. Korzystając z źródła danych (zapytanie SQL czy używając jakiego systemu ORM np. LINQ) bindujemy go z naszą kontrolką:

    1                 SqlConnection conn = new SqlConnection(@"Data Source=ACER5738\sqlexpress;Initial Catalog=AdventureWorks;Integrated Security=True");
    2                 SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM Person.Contact", conn);
    3                 DataSet ds = new DataSet();
    4                 da.Fill(ds, "Contact");
    5                 grid.DataSource = ds;
    6                 grid.DataBind();

Takie zbindowanie spowoduje wypełnienie DataGrida wszystkimi danymi z tabeli Contact. Wypełnienie to jest jak najbardziej poprawne o ile w tabeli nie ma olbrzymich ilości danych. W przypadku gdy mamy wyświetlić tysiące wierszy, użytkownik naszej aplikacji może odczuć narzut spowodowany przesłaniem ogromnych ilości danych.

Aby zachować dużą wydajność naszej aplikacji można zastosować stronicowanie. Dzięki niemu dane zostają podzielone na kilka stron tzn. na pierwszej stronie mamy wyniki od 1 do 20 wiersza naszej tabeli na drugiej 21 - 40 itd.

Istnieją dwie możliwości implementacji tego rozwiązania. Pierwsza oferowana przez kontrolkę oraz druga, którą należy samemu zaimplementować.

Zobaczmy najpierw na prostszą wersję tego rozwiązania czyli możliwości oferowane stricte przez kontrolkę. Aby włączyć Paging należy ustawić właściwośćAllowPaging na true. Dzięki właściwości PageSize ustalamy ile wierszy będzie widoczne na jednej stronie. Właściwość CurrentPageIndex określa numer wyświetlanej strony (licząc od 0). PageCount określa ilość wszystkich stron. Jeśli uruchomimy teraz nasz projekt to zobaczymy następujący ekran:
Kliknięcie na strzałkę odpowiedzialną za przewijanie stron nie zmienni niczego w wyświetlanej zawartości. Dzieje się tak dlatego, gdyż nie obsłużyliśmy zdarzenia PageIndexChanged:

    1         protected void grid_PageIndexChanged(object source, DataGridPageChangedEventArgs e)
    2         {
    3             SqlConnection conn = new SqlConnection(@"Data Source=ACER5738\sqlexpress;Initial Catalog=AdventureWorks;Integrated Security=True");
    4             SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM Person.Contact", conn);
    5             DataSet ds = new DataSet();
    6             da.Fill(ds, "Contact");
    7             grid.CurrentPageIndex = e.NewPageIndex;
    8             grid.DataSource = ds;
    9             grid.DataBind();
   10         }

Teraz po kliknięciu na strzałkę zawartość będzie się płynnie zmieniała.
Po przetestowaniu tego rozwiązania dla kilku tysięcy wierszy okazuje się, że aplikacja przyspieszyła znacząco. Jednak co jeśli do tabeli będą trafiały dziesiątki danych a jej rozmiar nie będzie liczony w dziesiątkach tysięcy a setkach?
Otóż rozwiązanie oferowane przez kontrolkę ma podstawowy błąd, który może nie być zauważony przez programistę na samym początku pracy z nią. Podczas każdego przeładowania strony DataSet jest wypełniany całą zawartością tabeli Contact. Najlepszym rozwiązaniem byłoby zaimplementowanie metody dzięki której pobierane byłyby tylko te rekordy które są aktualnie wymagane.

Własna implementacja stronicowania.
Najpierw zmieniamy właściwość AllowCustomPaging na true. Następnie musimy ustawić właściwość VirtualItemCoun, która informuje o ilości wszystkich wierszy do wyświetlenia. Można to uczynić za pomocą prostej funkcji SQLa:

    1                 SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM  Person.Contact", conn);
    2                 conn.Open();
    3                 int count = (int)cmd.ExecuteScalar();
    4                 grid.VirtualItemCount = count;
    5                 conn.Close();

Teraz należy obmyślić w jaki sposób dostać się do odpowiednich wierszy. Najprostsze rozwiązanie otrzymamy gdy w naszej tabeli jest pole Id Autoincrement. Wtedy możemy skorzystać z naszych właściwości i pobierać dane w taki sposób:

            SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM Person.Contact WHERE ContactID > " + (grid.PageSize * (e.NewPageIndex - 1)).ToString() + " AND ContactID <= " + (grid.PageSize * e.NewPageIndex).ToString(), conn);

Co jeśli w tabeli nie ma kolumny Autoincrement? Należy wtedy utworzyć tabelę tymczasową z dwoma kolumnami: pierwsza będzie autoincrement a druga np IDContact. Wypełniamy tabelę autoincrement danymi z tabeli Contact a następnie łączymy w zapytaniu obie tabele.

Zapytanie może wydać się skomplikowane, lecz jest skuteczne. Inną możliwością (dużo prostszą w implementacji) jest skorzystanie z zapytania LINQu:

    1             var db = new DataClasses1DataContext();
    2             var q = (from a in db.Contacts
    3                      select a).Skip(grid.PageSize * (e.NewPageIndex - 1)).Take(grid.PageSize);
    4             grid.CurrentPageIndex = e.NewPageIndex;
    5             grid.DataSource = q;
    6             grid.DataBind();

Instrukcja Skip pomija żądaną ilość wierszy w wyniku a instrukcja Take pobiera wskazaną ilość wierszy. Jeśli podglądnęlibyśmy zapytanie SQL które generuje LINQ:

{SELECT [t1].[ContactID], [t1].[NameStyle], [t1].[Title], [t1].[FirstName], [t1].[MiddleName], [t1].[LastName], [t1].[Suffix], [t1].[EmailAddress], [t1].[EmailPromotion], [t1].[Phone], [t1].[PasswordHash], [t1].[PasswordSalt], [t1].[AdditionalContactInfo], [t1].[rowguid], [t1].[ModifiedDate]
FROM (
SELECT ROW_NUMBER() OVER (ORDER BY [t0].[ContactID], [t0].[NameStyle], [t0].[Title], [t0].[FirstName], [t0].[MiddleName], [t0].[LastName], [t0].[Suffix], [t0].[EmailAddress], [t0].[EmailPromotion], [t0].[Phone], [t0].[PasswordHash], [t0].[PasswordSalt], [t0].[rowguid], [t0].[ModifiedDate]) AS [ROW_NUMBER], [t0].[ContactID], [t0].[NameStyle], [t0].[Title], [t0].[FirstName], [t0].[MiddleName], [t0].[LastName], [t0].[Suffix], [t0].[EmailAddress], [t0].[EmailPromotion], [t0].[Phone], [t0].[PasswordHash], [t0].[PasswordSalt], [t0].[AdditionalContactInfo], [t0].[rowguid], [t0].[ModifiedDate]
FROM [Person].[Contact] AS [t0]
) AS [t1]
WHERE [t1].[ROW_NUMBER] BETWEEN @p0 + 1 AND @p0 + @p1
ORDER BY [t1].[ROW_NUMBER]
}

wydaje się bardzo skomplikowane, jednak nie odbiega od tego które sami poprzednio skonstruowaliśmy.

Podsumowując CustomPaging przydaje się wtedy gdy nasze źródło danych zawiera dużo wyników oraz zależy nam aby użytkownik nie musiał czekać na załadowanie danych przez dłuższy czas.