piątek, 30 kwietnia 2010

Serializacja - kurs przygotowawczy do egzaminu 72-536

Dzisiaj bardzo krótki post. Ostatnio miałem okazję prowadzić prezentację w ramach szkolenia przygotowującego do egzaminu 72-536. Tematem była serializacja w środowisku .NET. W prezentacji zostało przedstawione serializowanie binarne, przy użyciu soap formatter, XML oraz sposób jak zaimplementować własną serializację. Prezentacja w formacie pdf. Link do prezentacji

poniedziałek, 26 kwietnia 2010

Task Parallel Library cz. 2

Aby możliwe było korzystanie z nowych udogodnień, należy zapoznać się z przestrzenią nazw System.Threading.Tasks. To w niej znajdują się klasy wspierające programowanie wielowątkowe.

Zacznijmy od klasy Parallel. Jest to statyczna klasa zawierająca 3 interesujące nas metody: For, ForEach oraz Invoke (oczywiście każda metoda zawiera po kilka przeładowań, zwierających różne parametry, typy, dodatkowe opcje).

For
Jest to pierwsza a zarazem jedna z najczęściej wykorzystywanych konstrukcji w nowej bibliotece. Spójrzmy na poniższy kod:

            //Standardowa pętla for
            for (int i = 0; i < 100; i++)
            {
                Console.Write(i + "  ");
            }

            //Pętla For z biblioteki TPL
            Parallel.For(0, 100, (i) =>
                {
                    Console.WriteLine(i + "  ");
                });

Jak widać dużych różnic nie ma. Po uruchomieniu programu zobaczymy:


Wywnioskować z tego możemy, że pętla zrównoleglona nie wykonuje zadań w kolejności. Należy więc to uwzględnić przy pisaniu swojego oprogramowania.

Zobaczmy teraz realny przykład, gdzie będzie można pokazać, że wykorzystując nową pętlę przyśpieszymy wykonywane operacje. Najprostszym przykładem, a zarazem najczęściej prezentowanym jest mnożenie macierzy. Zobaczmy na kod:

        public static void MMLinear(int[,] a, int[,] b, int[,] c, int size)
        {
            for (int i = 0; i < size; i++)
            {
                for (int j = 0; j < size; j++)
                {
                    int tmp = 0;
                    for (int r = 0; r < size; r++)
                    {
                        tmp += a[i, r] * b[r, j];
                    }
                }
            }
        }

Jak widać zwykłe linearne mnożenie. Teraz zobaczmy na wersję korzystającą z dobrodziejstw funkcji For:

        public static void MMTPL(int[,] a, int[,] b, int[,] c, int size)
        {
            Parallel.For(0, size, (i) =>
            {
                for (int j = 0; j < size; j++)
                {
                    int tmp = 0;
                    for (int r = 0; r < size; r++)
                    {
                        tmp += a[i, r] * b[r, j];
                    }
                }
            });
        }

Zmian jak widać nie ma wielkich poza pierwszą linijką, gdzie zamiast zwykłej pętli for mamy wykorzystaną funkcję For.
Kod samej procedury testowej wyglądał następująco:

            int size = 1000;
            int[,] a = new int[size, size];
            int[,] b = new int[size, size];
            int[,] c1 = new int[size, size];
            int[,] c2 = new int[size, size];

            Random r = new Random();
            for (int i = 0; i < size; i++)
            {
                for (int j = 0; j < size; j++)
                {
                    a[i, j] = r.Next();
                    b[i, j] = r.Next();
                }
            }

            Stopwatch s = new Stopwatch();
            s.Start();
            MatrixMultiplication.MMLinear(a, b, c1, size);
            s.Stop();
            Console.WriteLine(s.ElapsedTicks);
            Console.WriteLine();
            Thread.Sleep(2000);
            s.Restart();
            s.Start();
            MatrixMultiplication.MMTPL(a, b, c2, size);
            s.Stop();
            Console.WriteLine(s.ElapsedTicks);

Do mierzenia czasu korzystam tutaj z klasy Stopwatch. Gdy uruchomimy program zobaczymy następujące rezultaty:


Pierwszy wynik to mnożenie wykonane w zwykłej liniowej wersji funkcji. Druga wersja korzysta z zalet biblioteki TPL. Jak widać czas wykonywania operacji znacząco się skrócił (Intel Core T6400). Nie jest to 100% przyrost, ale śmiało można powiedzieć, że przy maszynach wyposażonych w większą ilość rdzeni przyrost szybkości wykonywania mnożenia macierzy będzie rósł. Przyrost szybkości można przeważnie liczyć na około 70 – 80%, co i tak jest niezłym wynikiem biorąc pod uwagę czas poświęcony na zastosowanie tego sposobu.
Spójrzmy jeszcze na deklarację Parallel.For:
For(Int32, Int32, Action)
Jest to najprostsza z konstrukcji. Podczas działania wykorzysta wszystkie dostępne rdzenie w naszym komputerze. Jeżeli chcielibyśmy mieć możliwość sami ustalić na ilu maksymalnie procesorach odbędą się obliczenia naszego zadania, możemy skorzystać z bardziej rozbudowanej konstrukcji:
For(Int32, Int32, ParallelOptions, Action)
Dla przykładu, dla naszego przykładu z macierzami:


Parallel.For(0, size, new ParallelOptions { MaxDegreeOfParallelism = 2 }, (i) =>
            {
                for (int j = 0; j < size; j++)
                {
                    int tmp = 0;
                    for (int r = 0; r < size; r++)
                    {
                        tmp += a[i, r] * b[r, j];
                    }
                }
            });

Wszystkie przeładowane wersje funkcji For zwracają rezultat jako obiekt typu ParallelLoopResult. Wiadomości zawarte w tym obiekcie mogą się przydać w przypadku wystąpienia wyjątku. Można wtedy sprawdzić, przy której iteracji doszło do wystąpienia wyjątku (dokładniej mówiąc jest to dolna granica wystąpienia przerwania – co to oznacza? Dla przykładu jeżeli mieliśmy 1000 iteracji a przerwanie nastąpiło przy 100 to iteracje 101 w górę nie powinny się wykonać ale te od 0 - 100 mogą nadal się wykonywać).


ForEach
Funkcja ForEach jest wielowątkowym odpowiednikiem pętli foreach:


            foreach (var item in collection)
            {

            }

            Parallel.ForEach(collection, Action);

Zobaczmy na deklarację tej metody:
ForEach(IEnumerable, Action)

Tak więc możemy śmiało posługiwać się funkcją w odniesieniu do wszystkich kolekcji implementujących interfejs IEnumerable. Przykład:

            List<int> lista = new List<int>();
            Random r = new Random();
            for (int i = 0; i < 50; i++)
            {
                lista.Add(r.Next(1000));
            }

            Parallel.ForEach(lista, (item) =>
                {
                    Console.WriteLine(item);
                });


Invoke
Ostatnia metoda zawarta w klasie Parallel to Invoke. Jak sama nazwa mówi, pozwala ona uruchomić funkcję lub tablicę funkcji. Deklaracja:
Invoke(Action[])

Przykład:


            List<Action> lista = new List<Action>();
            lista.Add(() => { Console.WriteLine("Task 1"); });
            lista.Add(() => { Console.WriteLine("Task 2"); });
            lista.Add(() => { Console.WriteLine("Task 3"); });
            lista.Add(() => { Console.WriteLine("Task 4"); });

            Array array = lista.ToArray();

            Parallel.Invoke((Action[])array);


Kilka słów odnośnie wychodzenia z pętli. Każdy wie, że z każdej pętli można wyjść wcześniej dzięki słowu kluczowemu break. Parallel.For i ForEach także mają taką możliwość. Do wykorzystania mamy dwie metody Stop i Break. Różnią się one tym, że Stop powiadamia o braku konieczności wykonywania następnych iteracji, natomiast Break gwarantuje że po aktualnej iteracji nie zostaną wykonane następne. Aby mieć możliwość korzystania omówionych funkcji należy przesłać do delegaty obiekt klasy ParallelLoopState:


            int x = 0;
            Parallel.For(0, 100, (int i, ParallelLoopState loop) =>
                {
                    x += 23;
                    if (x > 100)
                    {
                        loop.Stop();
                    }
                    Console.WriteLine(i);
                    Console.WriteLine(x);
                    Console.WriteLine("--------------------");
                });

Tyle jeżeli chodzi o pętle wielowątkowe. W następnej części trochę o klasie Task.

niedziela, 25 kwietnia 2010

Task Parallel Library cz. 1

Początkowo myślałem, aby tekst ten dodać jako trzecia część z cyklu programowanie wielowątkowe. Z drugiej strony TPL jest na tyle ciekawą co niezwykle bogatą biblioteką. Myślę, że należy poświęcić jej troszkę więcej miejsca niż tylko skwarkowy opis metody ForEach.

Programowanie wielowątkowe nigdy nie należało do łatwych. Wielu deweloperów unika stosowania go ze względu na trudności w debugowaniu aplikacji czy też trudne do wykrycia błędy powodowane przez źle zaprojektowane wątki.
Microsoft postanowił wyjść naprzeciw tym problemom i stworzył bibliotekę o nazwie Parallel Extensions. Pierwsza jej wersja została przedstawiona w listopadzie 2007 r. podczas CTP (Community Technology Preview). Następne wydania w grudniu 2007, styczniu 2008 potwierdziły tylko chęć Microsoft Research w dążeniu do stworzenia czegoś, co naprawdę ułatwi pracę programistom. Stworzona biblioteka nabiera znaczenia dopiero teraz. Została ona oficjalnie włączona do .NET 4.0 a z jej możliwości możemy korzystać w Visual Studio 2010 bez konieczności instalowania dodatkowych bibliotek. Wraz z nową biblioteką dostajemy nowe narzędzia ułatwiające debugowanie aplikacji wielowątkowych (np. Concurrency Visualizer). Całość biblioteki ładnie udokumentowano na MSDN oraz zamieszczono wiele przykładów użycia biblioteki.

Oprócz problemów dotyczących trudności w debugowaniu i projektowaniu aplikacji wielowątkowych dochodzi problem narzutu jaki niosą ze sobą wątki. Podczas tworzenia wątku na platformie .NET 4.0 rezerwowane jest 1 MB pamięci dla stosu, niezależnie od wielkości przekazywanych parametrów. Innym problemem jest ilość tworzonych wątków. Należy sobie zdać sprawę, że utworzenie 1000 wątków na maszynie wyposażonej w 4 rdzenie nie zwiększy szybkości wykonywania operacji a jedynie spowoduje większe zużycie zasobów. Dojdzie także problem przełączania między wykonującymi się wątki. W ogólności przy założeniu, że mamy 4 rdzenie i 1000 wątków może dojść do sytuacji kiedy nasze zadanie wykona się wolniej niż przy korzystaniu z aplikacji sekwencyjnej. Najlepszą wydajność uzyskujemy, gdy dla jednego rdzenia wykonuje się jeden wątek.

W .NET ilość logicznych procesorów można uzyskać za pomocą kodu:

            Environment.ProcessorCount;

Wykorzystując tą informację, możemy w logiczny sposób podzielić pracę pomiędzy wątkami. Pojawia się jednak mały problem tutaj. Wyobraźmy sobie tablicę zadań. Powiedzmy że mamy 20 zadań. Pierwsze zadanie zajmuje 1 sek., drugie 2 sek., trzecie 3 sek., itd. Całkowity czas wykonania wszystkich zadań to 210 sek. Pierwsze co nam rzuca się na myśl to podzielenie tablicy zadań na dwie równe. Pierwszy wątek mógłby wykonywać zadania od 1 do 10 a drugi od 11 do 20. Korzystając z takiego rozwiązania otrzymujemy jednak wersję w której pierwszy wątek wykona swoje zadania po 55 sek. a drugi po 155 sek. Podział więc nie jest za dobry. Kolejnym rozwiązaniem mógłby być podział naprzemienny:


Przy takim założeniu, 1 wątek będzie pracować przez 100 sek., gdy tymczasem drugi 110. Podział jest dużo lepszy od pierwszego pomysłu.

Jak w .NET zrealizowany jest ten problem?
Zobaczmy na diagram:


Jak widać na diagramie istnieje jedna główna kolejka zadań. Z tej kolejki zadania następnie przesyłane są na mniejsze kolejki zadań dla każdego z procesorów. Kolejki te mogą miedzy sobą wymieniać się informacjami. Dla przykładu jeżeli Core 1 (procesor/rdzeń 1) wykonał już wszystkie zadania które miał w swojej kolejce, może poszukać zadań do realizacji u sąsiada (Core 2). Można teraz domyślić się, że wykonywane zadania mogą nie odbywać się w takiej kolejności w jakiej chcielibyśmy. Przypuszczając że mamy 400 zadań i 4 rdzenie nie mamy pewności, że każdy z rdzeni wykona 100 zdań. Należy więc mieć na uwadze gdy projektujemy metody, które wykorzystamy dla TPL. Kolejnym aspektem jest wykrywanie wyjątków. Należy sobie zdać pytanie: co w momencie gdy dojdzie do wyjątku? TPL zostało tak zaprojektowane, że gdy dojdzie do wystąpienia wyjątku podczas wykonywania zadania na jednym z rdzeni, TPL wyrzuci wyjątkiem i anuluje zadania. TPL nie gwarantuje, że w momencie wyrzucenia wyjątku przestanie wykonywać zadania. Komunikacja pomiędzy kolejkami dla zadań jest tutaj kluczowym aspektem. Co w przypadku gdy na wszystkich rdzeniach wystąpi wyjątek? Zostanie rzucony inny wyjątek uwzględniający tę sytuację.

sobota, 24 kwietnia 2010

Globalizacja w ASP.NET

Internet to ogromna społeczność, zrzeszająca użytkowników z całego świata. Odwiedzający naszą stronę internauci, mogą mieszkać w różnych krajach i posługiwać się różnymi językami. Odwiedzając naszą stronę, z pewnością chcieliby przeczytać zawarte na niej informacje w swoim ojczystym języku.
ASP.NET oferuje bardzo prosty mechanizm, pozwalający na uzyskanie wielojęzyczności naszej strony – globalizacja.
Globalizację w ASP.NET uzyskujemy dzięki plikom typu resource. W pliku takim znajduje się tekst strony dla danego języka. Użytkownik wchodząc na stronę może wybrać lub poprzez jego ustawienia przeglądarki zostanie dla niego automatycznie wybrany język o ile jest dostępny. Jeżeli nie ma, zostanie wybrany domyślny język dla naszej strony. Tworzenie plików resorce powinno podlegać odpowiedniemu nazewnictwu ich. Mając stronę nazwaną WebForm1.aspx plik resursa powinien być nazwany: WebForm1.aspx.resx. Takie nazwanie powoduje, że będzie to także domyślnie ładowany plik. Co to oznacza dla nas? Jeśli ktoś z Japonii będzie chciał się dostać na naszą stronę, a my nie będziemy mieli naszej strony przetłumaczonej na ten język, użytkownik zostanie automatycznie przekierowany do pliku domyślnego. Aby stworzyć plik resource dla specyficznego języka używamy następującego schematu: .aspx..resx czyli np. dla podanego wyżej przykładu i języka niemieckiego mamy: WebForm1.aspx.de.resx.
Visual Studio pozwala na bardzo szybkie i łatwe generowanie takich plików. Zobaczmy na przykład poniższej aplikacji:








Aby wygenerować plik klikamy kolejno (należy być w trybie design): Options -> Tools -> Generate Local Resource. Powoduje to kolejno:
- stworzenie folderu App_LocalResource (o ile jeszcze nie został stworzony)
- wygenerowanie pliku resource, zawierającego wszystkie widzialne (wyświetlane) teksty (caption, text, tooltip itp.)

Plik ten można podglądnąć klikając na niego dwukrotnie:


Plik który otworzyliśmy jest plikiem XML. Można go edytować dowolnym edytorem XMLa czy nawet zwykłym notatnikiem. Większość wpisów to węzły typu nazwa – wartość (nazwa kontrolki na formie – wyświetlany tekst). Ważne jest tu stwierdzenie „nazwa kontrolki” – oznacza to tyle, że jeżeli wprowadzimy jakiś tekst w czystej formie (nie korzystając z kontrolki) tekst ten nie zostanie uwzględniony w pliku resource. Należy więc mieć to na uwadze i dla własnej wygody stosować wszędzie gdzie się da kontrolki.

Przypisanie kontrolek do konkretnego pliku resource odbywa się poprzez dodanie znacznika XML meta:resourcekey do atrybutów danej kontrolki. Przykładowo dla kontrolki TextBox będzie to:


Ważne jest pamiętać, że Visual Studio nie będzie uaktualniał pliku resource. Po dodaniu nowej kontrolki, należy samemu się zatroszczyć o to, żeby ją poprawnie interpretował nasz zasób. Najłatwiej wykonać pliki resource na końcu pracy z projektem.

Aby dodać kolejny język do naszego projektu, kopiujemy plik resource, który jest domyślnym, wklejamy go do tego samego folderu i odpowiednio go nazywamy. Następnie zmieniamy wewnątrz tego pliku pola odpowiedzialne za wyświetlanie tekstu. Dla przykładu stworzyłem na podstawie domyślnego języka, plik dla użytkowników anglojęzycznych:


Oczywiście, wybór kultury możemy powierzyć użytkownikowi. Aby programistycznie przypisać język aplikacji danemu plikowi resorce, należy podczas wywoływania metody InitializeCulture odpowiednio przypisać właściwości UICulture język:

        protected override void InitializeCulture()
        {
            switch (languageId)
            {
                case 1:
                    UICulture = "pl";
                    break;
                case 2:
                    UICulture = "en";
                    break;
                default:
                    break;
            }
            base.InitializeCulture();
        }


To wszystko co jest wymagane, aby z łatwością korzystać z wielojęzyczności w aplikacjach ASP.NET

czwartek, 22 kwietnia 2010

Programowanie wielowątkowe w .NET cz. 2

Programowanie aplikacji z graficznym interfejsem jest obecnie podstawową techniką każdego programisty. Środowiska takie jak Visual Studio oferują bogate możliwości tworzenia graficznego interfejsu jak np. WindowsForms czy też Windows Presentation Fundation (WPF).
Często zdarza się, że aplikacja wykonując skomplikowane zadanie zamiera. Należy ograniczyć takie zjawisko. Jednym z rozwiązań jest skorzystanie z wątków. Poprzednia część artykułu, ukazuje podstawy pracy z wątkami. Tworzenie, Anulowanie oraz synchronizację pomiędzy nimi. W tej części skupimy się na wykorzystaniu wątków w UI takim jak WindowsForms czy też WPF.

Zacznijmy od krótkiego przykładu:
Stworzymy prostą aplikację WindowsForms. Na formatkę kładziemy Button i wprowadzamy dla niego

        private void button1_Click(object sender, EventArgs e)
        {
            string s = "";
            for (int i = 0; i < 100000; i++)
            {
                s += "a";
            }
            Text = "done";
        }

Po uruchomieniu tej aplikacji i naciśnięciu przycisku, aplikacja zamiera. Użytkownik najprawdopodobniej przerwałby aplikację. Co więc zrobić aby interfejs nie zamarł? Tworzymy wątek:

    public partial class Form1 : Form
    {
        Thread t1;
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            t1 = new Thread(Operation);
            t1.Start();
        }

        private void Operation()
        {
            string s = "";
            for (int i = 0; i < 100000; i++)
            {
                s += "a";
            }
            Text = "done";
        }
    }

Po uruchomieniu aplikacji i kliknięciu na button otrzymamy następujący błąd:


Dla wielu taki błąd może wydać się mało zrozumiały i w pierwszym momencie całkowicie irracjonalny. W wolnym tłumaczeniu, można by powyższy komunikat przetłumaczyć jako: Niedozwolona operacja między wątkowa: uzyskanie dostępu do Form1 z innego wątku niż została utworzona. Otóż błąd nie jest nowością i znają go wszyscy, którzy zaczynali kiedyś swoją przygodę z programowaniem wielowątkowym. Otóż wszystko co dzieje się na formie działa na jednym wątku. Jak wiemy już z wcześniejszej części operacje między wątkowe które modyfikują dane są niebezpieczne. Dlatego też otrzymujemy owy błąd. Aby się go pozbyć, należy uaktualnić formę z wątka w którym została stworzona.
Operacja która nam to umożliwia jest powtarzalna i w większości przypadków będzie za każdym razem w taki sam sposób przebiegała:
1. Tworzymy metodę odpowiedzialną za uaktualnienie interfejsu użytkownika
2. Tworzymy delegatę typu takiego, jak metoda do uaktualnienia interfejsu
3. Sprawdzamy za pomocą właściwości InvokeRequired czy jesteśmy w wątku Formy
4. Jeżeli nie wywołujemy metodę Invoke
5. W przeciwnym razie uaktualniamy interfejs

Poprawny kod programu:

    public partial class Form1 : Form
    {
        Thread t1;
        private delegate void Update(string s);
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            t1 = new Thread(Operation);
            t1.Start();
        }

        private void Operation()
        {
            string s = "";
            for (int i = 0; i < 100000; i++)
            {
                s += "a";
            }
            UpdateUI("done");
        }

        private void UpdateUI(string text)
        {
            if (this.InvokeRequired)
            {
                Invoke(new Update(UpdateUI), new object[] { text });
            }
            else
            {
                this.Text = text;
            }
        }
    }

Korzystając z platformy .NET mamy jeszcze możliwość użycia SynchronizationContext (.NET 2.0) oraz wyrażeń lambda (.NET 3.0).

SynchronizationContext
Możliwość komunikacji za pomocą tej metody została wprowadzona w .NET 2.0. Jeżeli mamy dwa wątki, przykładowo t1 i t2, to jeśli chcielibyśmy w pewnym momencie z wątka t1 wykonać jakiś kod w wątku t2 możemy skorzystać z metody SynchronizationContext.Send(). Jest tu jedno ale: nie każdy wątek posiada SynchronizationContext. Na nasze szczęście każdy UI posiada taki obiekt, dlatego też bez obaw możemy za jego pomocą aktualizować interfejs użytkownika. Może nasunąć się pytanie: skąd bierze się w wątku owy tajemniczy obiekt SynchronizationContext? Odpowiedź nie jest trudna i nie ma tu żadnej magii i setek linijek kodu. SynchronizationContext tworzy pierwsza utworzona kontrolka. Spójrzmy na poniższy kod:

    public partial class Form1 : Form
    {
        Thread t;
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            t = new Thread(DoSomething);
            t.Start(SynchronizationContext.Current);
        }

        private void DoSomething(object o)
        {
            for (int i = 0; i < 10000; ++i)
            {
                SynchronizationContext cntx = o as SynchronizationContext;
                cntx.Send(UpdateUI, i);
            }
        }

        private void UpdateUI(object i)
        {
            listBox1.Items.Add(i);
            label1.Text = listBox1.Items.Count.ToString();
        }
    }

Na formatce znajduje się label, button oraz listbox. Po naciśnięciu przycisku tworzymy nowy wątek i przekazujemy mu obiekt SynchronizationContext. Jako że klasa jest tworzona w wątku w którym działa forma, przekażemy wątek UI, czyli właściwy dla nas. Następnie do pracy rusza metoda DoSomething, która w pętli dodaje 10000 elementów do listboxa. Za pomocą metody Send wysyłamy zmiany do naszych kontrolek. Można by zadać pytanie: czy jest jakaś różnica pomiędzy tym sposobem uaktualniania interfejsu a przedstawionym poprzednio? Jednym z dużych plusów tego sposobu jest to, że nie sprawdzamy dwukrotnie tego, czy jesteśmy w wątku UI. Oprócz metody Send jest jeszcze metoda Post. W jej przypadku problemem jest jednak to, że nie mamy możliwości łapania błędów w uruchomionym wątku, a jakikolwiek błąd spowoduje wywołanie błędu w wątku UI.

Wyrażenia Lambda
Zamiast używać anonimowych delegat, moglibyśmy użyć wyrażenia lambda. Wyrażenia lambda wprowadzono w .NET 3.0. Pozwalają one na zminimalizowanie kodu w wielu miejscach długiego programu. Są też i wady tego rozwiązania: często czynią kod mniej czytelnym. Zobaczmy nasz przykład z wyrażeniem lambda:

    public partial class Form1 : Form
    {
        Thread t;
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            t = new Thread(DoSomething);
            t.Start(SynchronizationContext.Current);
        }

        private void DoSomething(object o)
        {
            for (int i = 0; i < 10000; ++i)
            {
                SynchronizationContext cntx = o as SynchronizationContext;
                Random r = new Random();
                cntx.Send( (x) =>
                    {
                        listBox1.Items.Add(i);
                        label1.Text = listBox1.Items.Count.ToString();
                    }, i);
            }
        }
    }

BackgroundWorker
Jest to jedno z udogodnień wprowadzonych w .NET 2.0. Komponent pozwala na uruchomienie czynności w oddzielnym wątku. Rozwiązanie to korzysta z omówionego wcześniej SynchronizationContext, który pobiera z aktywnej formy. Oprócz tego oferuje takie udogodnienia jak raportowanie postępu operacji, przerwanie prowadzonych obliczeń.
Przykład:


    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

        }

 

        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)

        {

            for (int i = 0; i < 101; i++)

            {

                if (backgroundWorker1.CancellationPending)

                {

                    e.Cancel = true;

                    return;

                }

                backgroundWorker1.ReportProgress(i);

                Thread.Sleep(100);

            }

            e.Result = "done";

        }

 

        private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)

        {

            progressBar1.Value = e.ProgressPercentage;

        }

 

        private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)

        {

            if (e.Error == null && e.Cancelled == true)

            {

                return;

            }

            Text = e.Result.ToString();

        }

 

        private void button1_Click(object sender, EventArgs e)

        {

            backgroundWorker1.RunWorkerAsync();

        }

 

        private void button2_Click(object sender, EventArgs e)

        {

            backgroundWorker1.CancelAsync();

        }

    }


Przykład umożliwia uruchomienie wątku, który wykonuje fikcyjne obliczenia. Wątek można przerwać za pomocą przycisku stop. Wykorzystanie BackgroundWorkera jest proste i intuicyjne. Microsoft tworząc go miał właśnie prostotę na uwadze. Programowanie wielowątkowe przez wielu jest uważane za jedno z trudniejszych w kwestii implementacji jak i debugowania.


WPF
W WPF tak samo jak i WindowsForms obowiązuje zasada, że dostęp do kontrolek jest dozwolony tylko z wątku w którym zostały utworzone.
W WPF możemy skorzystać tak jak poprzednio z BackgroundWorkera:
Kod:

<Window x:Class="WpfApplication2.MainWindow"

       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

       Title="MainWindow" Height="350" Width="525">

    <Grid>

        <Grid.RowDefinitions>

            <RowDefinition Height="30"/>

            <RowDefinition Height="50"/>

            <RowDefinition Height="30"/>

            <RowDefinition Height="*"/>

 

        </Grid.RowDefinitions>

 

        <Grid.ColumnDefinitions>

            <ColumnDefinition Width="80"/>

            <ColumnDefinition Width="300" />

        </Grid.ColumnDefinitions>

 

        <Button Name="button1" Grid.Column="0" Grid.Row="0" Content="Button" Click="button1_Click"/>

        <ProgressBar Name="progressBar" Minimum="0" Maximum="100" Value="0" Grid.Column="1" Grid.Row="2"/>

 

    </Grid>

</Window>

 


    public partial class MainWindow : Window

    {

        private BackgroundWorker bw;

        public MainWindow()

        {

            InitializeComponent();

            bw = new BackgroundWorker();

            bw.DoWork += new DoWorkEventHandler(bw_DoWork);

            bw.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged);

            bw.WorkerReportsProgress = true;

        }

 

        void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)

        {

            progressBar.Value = e.ProgressPercentage;

        }

 

        void bw_DoWork(object sender, DoWorkEventArgs e)

        {

            for (int i = 0; i < 101; i++)

            {

                Thread.Sleep(100);

                bw.ReportProgress(i);

            }

        }

 

        private void button1_Click(object sender, RoutedEventArgs e)

        {

            bw.RunWorkerAsync();

        }

    }


Oprócz użycia BackgroundWorkera WPF posiada bardzo podobny mechanizm jaki był zaimplementowany w początkowych wersjach .NET. Jest to jednak bardziej kompleksowe rozwiązanie. Należy zdać sobie sprawę, że wszystkie obiekty WPF, które należy traktować z uwagą podczas pracy z wątkami dziedziczą po klasie DispatcherObject. Dzięki temu wiemy, że Button czy Brush podczas operacji między wątkowych muszą być traktowane w specjalny sposób a klasa Color już nie, gdyż nie dziedziczy po DispatcherObject. Obiekt DispatcherObject zawiera kilka metod które pozwalają określić czy znajdujemy się we właściwym wątku. CheckAccess oraz VerifyAccess – te dwie metody pozwalają na określenie czy znajdujemy się w dobrym wątku. Pierwsza metoda, jest podobna do właściwości znanej z WindowsForms InvokeRequired. Drga z metod pozwala na upewnienie, czy po przejściu na właściwy w naszym mniemaniu wątek, przebiegło pomyślnie. Wywoływanie VerifyAccess nie jest konieczne za każdym razem, a większość kontrolek samo za nas dba o kontrolę podczas ich używania. Dzieje się tak dlatego, gdyż wywoływanie tej metody obniża wydajność aplikacji.
Jeśli już wiemy jak sprawdzić jak sprawdzić czy znajdujemy się w odpowiednim wątku, warto wiedzieć jak skorzystać z niego. Klasa DispatcherObject oferuje właściwość o nazwie Dispatcher. Za pomocą niej możemy użyć takich metod jak Invoke oraz BeginInvoke. Różnica pomiędzy tymi metodami jest taka, że po wywołaniu metody Invoke, że wyjście z metody wykonywanej następuje dopiero po jej zakończeniu a dla metody BeginInvoke tworzona jest kolejka zadań (jednym słowem po wywołaniu metody, BeginInvoke nie czeka na zakończenie wywoływanej metody).
Spójrzmy na poprzedni przykład, tylko tym razem wykorzystamy metodę BeginInvoke:

    public partial class MainWindow : Window

    {

        private Thread T1;

        public MainWindow()

        {

            InitializeComponent();

        }

 

        private void button1_Click(object sender, RoutedEventArgs e)

        {

            T1 = new Thread(DoSomething);

            T1.IsBackground = true;

            T1.Start();

        }

 

        private void DoSomething()

        {

            for (int i = 0; i < 101; i++)

            {

                Thread.Sleep(100);

                progressBar.Dispatcher.BeginInvoke((Action)(() =>

                    {

                        progressBar.Value = i;

                    }), null);

            }

        }

    }


Należy pamiętać, że nie ma sensu wykonywać obliczeń w metodzie przekazywanej do BeginInvoke. Spójrzmy:

        private void DoSomething()

        {

            for (int i = 0; i < 101; i++)

            {

                progressBar.Dispatcher.BeginInvoke((Action)(() =>

                    {

                        Thread.Sleep(100);

 

                        progressBar.Value = i;

                    }), null);

            }

        }


W tym przypadku nic nie zyskujemy. BeginInvoke wywołuje się na wątku na którym działa ProgressBar, co za tym idzie interfejs użytkownika. Wykonywane obliczenia powodują zamrożenie interfejsu.
Wspomniałem wcześniej, że Invoke wykonując metodę nie kolejkuje zadań, a kończy swoje działanie dopiero w momencie zakończenia wywoływanej metody. Taka cecha może być przydatna gdy np. w wątku wyświetlamy okienko z odpowiedziami TAK/NIE i w zależności od odpowiedzi wykonujemy bądź kończymy metodę wykonywaną w wątku.


Tyle o wykorzystaniu wątków w aplikacjach z wizualnym interfejsem. Ostatnia część cyklu będzie dotyczyła nowości jaką wprowadził Microsoft do .NET 4.0 czyli ParallelExtensions

Źródło:
Pro WPF in C# 2008 Matthew MacDonald
Programming WPF Chris Sells and Ian Griffiths
http://www.codeproject.com/KB/threads/mtguide_1.aspx
http://www.codeproject.com/KB/threads/SynchronizationContext.aspx
http://www.codeproject.com/KB/cpp/BackgroundWorker_Threads.aspx
http://www.codeproject.com/KB/threads/ThreadingDotNet5.aspx
http://www.codeproject.com/KB/threads/mtguide_1.aspx
http://msdn.microsoft.com/en-us/library/ms741870.aspx

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