sobota, 3 lipca 2010

Task Parallel Library cz. 3

Dzisiaj co nieco o klasie Task i jej wykorzystaniu w programowaniu wielowątkowym.

Task jest bardzo podobny do Thread. Właściwie cały model programowania wielowątkowego jest taki sam. Zaletą Tasków jest jednak zarządzanie nimi, bogate API oraz mniejsza zachłanność zasobów niż Thready.

Task możemy stworzyć na dwa sposoby. Poprzez konstruktor lub poprzez Task.Factory.StartNew(...); Obydwie koncepcje mają swoje zalety i wady i należy zastanowić się którą koncepcję wybierzemy.
W większości przypadków należy skorzystać z drugiej metody (Task.Factory.StartNew(...);). Dzięki użyciu tego sposobu unikniemy sprawdzania czy Task został już stworzony a co za tym idzie osiągamy lepszą wydajność.
Pierwszy sposób jest lepszy w przypadku tworzenia własnej klasy, która dziedziczy po Task, czy też gdy w samym Tasku chcemy wykorzystać referencję do niego samego.

            //First way creating Task
            Task s1 = new Task(...);
            //Second way creating Task
            Task s2 = Task.Factory.StartNew(...);

Przejdźmy do jakiegoś prostego przykładu zastosowania Tasków:

            Task t1 = Task.Factory.StartNew(() =>
                {
                    for (int i = 0; i < 10; i++)
                    {
                        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                        Thread.Sleep(800);
                    }
                });
            Task t2 = Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                    Thread.Sleep(600);
                }
            });
            Task.WaitAll(t1, t2);

Po wykonaniu powyższego kodu otrzymamy na ekranie następujący wynik:


Na ekranie zostały wydrukowane id stworzonych wątków. Jak widać zostały stworzone dwa wątki. Jeśli utworzymy kolejne zadania, zaobserwujemy, że działają tylko dwa wątki na raz. Nie są tworzone następne. W moim przypadku dzieje się tak dlatego, ponieważ w komputerze na którym uruchamiam kod znajdują się dwa procesory logiczne.
Zastosowano tu także metodę WaitAll(Task[] tasks) która powoduje, że główny wątek poczeka aż do zakończenia wykonywania wszystkich aktualnie dodanych wątków. Możemy także zastosować konstrukcję Task.WaitAny(Task[] tasks) która powoduje zaczekanie na zakończenie któregokolwiek z Tasków.

Kolejną ciekawą metodą jest ContinueWith. Zobaczmy na przykład:

            Task t1 = Task.Factory.StartNew(() =>
                {
                    for (int i = 0; i < 10; i++)
                    {
                        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                        Thread.Sleep(800);
                    }
                });

            Task t2 = Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                    Thread.Sleep(600);
                }
            }).ContinueWith((x) =>
                {
                    Console.WriteLine("Task 2 has finished the work");
                });
            Task.WaitAny(t1, t2);

Metoda ContinueWith zostaje wywołana zaraz po tym jak swoją pracę zakończy Task t2. Jest to bardzo przydatna opcja, jeżeli mamy zamiar wykonywać zadania w określonej kolejności.
Oprócz tego do metody ContinueWith możemy przesłać parametry odpowiedzialne za określneie kiedy dana czynność ma być wykonana. Dla przykładu:

            Task t1 = Task.Factory.StartNew(() =>
                {
                    for (int i = 0; i < 10; i++)
                    {
                        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                        Thread.Sleep(800);
                    }
                });

            Task t2 = Task.Factory.StartNew(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                    Thread.Sleep(600);
                }
            }).ContinueWith((x) =>
                {
                    Console.WriteLine("Task 2 has finished the work");
                }, TaskContinuationOptions.NotOnFaulted);
            Task.WaitAny(t1, t2);

TaskContinuationOptions.NotOnFaulted - spowoduje, że zadanie zostanie wykonane tylko wtedy gdy t2 wykona swoje zadanie bez powodowania błędów.


Kolejnym ciekawym aspektem Tasków jest możliwość zwracania wartości. Zobaczmy na przykład:

            Task<int> countSomething = Task.Factory.StartNew<int>(() =>
                {
                    int tmp = 0;
                    for (int i = 0; i < 20000; i++)
                    {
                        for (int j = 0; j < i; j++)
                        {
                            if (i %2 == 0)
                            {
                                tmp = i - j / 2;
                            }
                        }
                    }
                    return tmp;
                });

            Console.WriteLine(countSomething.Result);

Odpytując właściwość Result otrzymujemy wynik działania Tasku. Należy pamiętać, że jeżeli wynik nie będzie jeszcze obliczony, nasz program zaczyma się w miejscu
Console.WriteLine(countSomething.Result);
dopóki wynik nie będzie gotowy.

Bardzo ciekawym składnikiem TPL jest klasa TaskScheduler. Pozwala ona na zarządzanie zadaniami tj. kolejnością ich wykonywania, priorytetami itp. W większości przypadków wystarcza domyślna konfiguracja. Jednym z ciekawszych zastosowań jest uaktualnianie UI (user interface) za pomocą TaskScheduler. Dzięki temu możemy uniknąć pisania kodu powodującego przejście do wątku UI; przykład dla aplikacji Windows Forms:

        private void button1_Click(object sender, EventArgs e)
        {
            TaskScheduler ts = TaskScheduler.FromCurrentSynchronizationContext();
            Task.Factory.StartNew(() =>
                {
                    string s = "";
                    for (int i = 0; i < 10; i++)
                    {
                        s += i.ToString();
                    }
                    return s;
                }).ContinueWith((x) =>
                    {
                        textBox1.Text = x.Result;
                    }, ts);
        }

Ostatnią rzeczą którą omówię w tym wpisie, to możliwość przerywania działania aktywnego Tasku. Jeżeli chcielibyśmy przerwać długotrwałą operację, która zużywa dużo zasobów sprzętowy można to zrobić za pomocą Token Cancellation. Rzecz jest bardzo prosta i najlepiej wyjaśni ją przykład:

            CancellationTokenSource source = new CancellationTokenSource();

            CancellationToken cancel = source.Token;

            Task<int> countSomething = Task.Factory.StartNew<int>(() =>

                {

                    int tmp = 0;

                    try

                    {

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

                        {

                            if (cancel.IsCancellationRequested)

                            {

                                Console.WriteLine("Operation was canceled");

                                throw new OperationCanceledException(cancel);

                            }

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

                            {

                                if (i % 2 == 0)

                                {

                                    tmp = i - j / 2;

                                }

                            }

                        }

                    }

                    catch(OperationCanceledException e)

                    {

                        Console.WriteLine("Catch error here");

                    }

                    return tmp;

                }, cancel);

            Thread.Sleep(200);

            source.Cancel();

            Console.ReadLine();


Jak widać z przykładu, najpierw poieramy Token z źródła. Uruchamiamy nasz program, główny wątek czeka 200 ms i po tym czasie uruchamia metodę Cancel(). W międzyczasie w pętli sprawdzamy warunek IsCancellationRequested == true. Jeżeli jest prawdziwy to znaczy że Task ma zostać przerwany i rzuca wyjątkiem OperationCanceledException - który następnie możemy przechwycić i obsłużyć.

Tyle szczegółów na temat klasy Task. Dużo innych informacji można znaleźć na MSDNie oraz blogu programistów TPL.



Źródło:
http://blogs.msdn.com/b/pfxteam/archive/2010/06/13/10024153.aspx
http://msdn.microsoft.com/en-us/library/system.threading.tasks.taskcontinuationoptions.aspx
http://blogs.msdn.com/b/pfxteam/archive/2009/04/14/9549246.aspx
http://msdn.microsoft.com/en-us/library/system.threading.tasks.taskscheduler.aspx
Introducing .NET 4.0 With Visual Studio 2010 Alex Mackey

Brak komentarzy:

Prześlij komentarz