wtorek, 23 listopada 2010

O Task–ach słów kilka...

Jak wcześniej pisałem, w .NET 4 Microsoft dodał do Frameworka naprawdę potężne narzędzie do tworzenia aplikacji wielowątkowych. Bibliotekę tę można podzielić na dwie części – PLINQ, o której pisałem we wcześniejszych postach oraz TPL (Task Parallel Library), o której chciałbym napisać parę słów.

TPL, została oczywiście zbudowana na wcześniej istniejących elementach Frameworka, takich jak wątki i pula wątków (klasy Thread i ThreadPool) i  jak sama nazwa wskazuje, bazuje na zadaniach, czyli obiektach reprezentowanych przez klasę Task. Sama biblioteka została umieszczona w namespace System.Threading.Tasks. Sam sposób zarządzania wielowątkowością został znacznie uproszony. Używając wcześniejszych wersji .NET-a podstawową strukturą był wątek, który można sobie wyobrazić jako swego rodzaju “robotnika” - trzeba go ciągle nadzorować, pilnować, aby coś złego się z nim nie stało, itd. W przypadków zadań (Task) jest zupełnie inaczej – w tym przypadku patrzymy bardziej na to, co jest do wykonania, niż jak należy to zrealizować.

Tworzenie zadań

Koniec teorii, czas na trochę praktyki – zadanie tworzymy wywołując oczywiście odpowiedni konstruktor klasy Task – polecam przeglądnięcie manuala, gdyż istnieje kilka, jak nie kilkanaście konstruktorów, które dają szerokie spektrum możliwości. Aby uruchomić tak utworzone zadanie wołamy na wcześniej utworzonym obiekcie metodę Start. Zadanie możemy też za jednym zamachem utworzyć i wystartować używając do tego celu statycznego property na klasie Task o nazwie Factory i wołając na nim metodę StartNew, która podobnie jak konstruktora klasy Task ma kilkanaście różnych przeciążeń. Poniżej przykład prezentujący wcześniej opisany mechanizm tworzenia zadań:

// Najprostszy sposób utworzenia zadania
Task t1 = new Task(state => DoActionAsync(state), "state value");
// I wystartowanie go:
t1.Start();
// Utworzenie zadania i wystartowanie:
Task t2 = Task.Factory.StartNew(() => DoOtherAsyncAction());
Przewagą (jedną z wielu oczywiście :) ), jaką mają zadania nad wątkami jest możliwość zwrócenia wartości. Pozwala na to klasa Task<TResult> dziedzicząca po wcześniej opisanej klasie Task. Konstruktor tego typu jako parametr przyjmuje funkcję, która zwraca rezultat typu TResult. Do wyniku tak stworzonego zadania możemy uzyskać dostęp korzystając z property Result. O czym należy pamiętać, to że właściwość ta jest synchroniczna i jeśli wywołamy akcesor to będzie on czekał na zakończenie wątku zadania i dopiero wtedy zwróci wynik. Poniżej przykład:
// Utworzenie zadania, które zwróci wynik:
Task t3 = new Task(() => GenerateSum());
// Zadanie możemy też uruchomić w bieżącym wątku
t3.RunSynchronously();
// Zadanie zwracające wynik możemy utworzyć i od razu uruchomić:
Task t4 = Task.Factory.StartNew(() => FindWord());

// Przetwarzanie w wątku głównym...

// Odczytujemy wynik z uruchomionego zadania:
String word = t4.Result;
Czekanie na zakończenie zadania

Częstą sytuacją jest czekanie w głównym wątku zakończenie zadania/zadań, które zostały uruchomione asynchronicznie (np. kontynuacja przetwarzania zależy od wyniku zadania). Jeśli czekamy na pojedyncze zadanie, możemy na jego instancji zawołać metodę Wait. Klasa Task dostarcza także statycznych metod do czekania wszystkie zdania z kolekcji (WaitAll) lub na pierwsze zadanie, które się zakończy (WaitAny):

Task t1 = Task.Factory.StartNew(() => DoAsync());
//Czekamy na zakończenie zadania
t1.Wait();

Task[] tasks = new Task[] {
    Task.Factory.StartNew(() => FirstMethod()),
    Task.Factory.StartNew(() => SecondMethod()),
    Task.Factory.StartNew(() => ThirdMethod())
};

// Czekamy na zakończenie wszystkich zadań:
Task.WaitAll(tasks);

I na koniec ciekawostka – TPL bardzo dba o wydajność i stara się nie tworzyć nowych wątków jeśli nie jest to konieczne. Jeśli więc wywołamy metodę Wait, zanim zadanie zostanie uruchomione, to framework nie będzie tworzył osobnego wątku dla zadania, tylko wykona je w wątku, który wywołał metodę Wait.

Linki
  1. Task Parallel Library
  2. Ogólnie o zadaniach

sobota, 20 listopada 2010

Słownik polsko-angielski dla Kindle’a

Od ponad roku jestem właścicielem czytnika Amazon Kindle. Urządzenie jest naprawdę świetne – niestety ma (przynajmniej dla mnie) jedną wadę – brakuje mu słownika polsko-angielskiego. Nigdzie nie udało mi się znaleźć odpowiedniego dokumentu, dlatego też postanowiłem sam taki słownik stworzyć.

Słowników online jest multum, zatem jedyne co należało zrobić, to stworzyć program, który używając odpowiednio dużej bazy angielskich słów pobierze z tych słowników tłumaczenia odpowiednio wygenerowany żądaniami http. Kolejny krok to zbudowanie z wcześniej przygotowanej bazy tłumaczeń pliku “zrozumiałego” dla Kindle’a. Na szczęście czytnik Amazona odczytuje pliki .mobi, które każdy może bardzo łatwo przygotować. Pliki takie tworzy się używając prawie identycznych znaczników, jak w stronach html, a następnie przy użyciu darmowego narzędzia można je skompilować do formatu mobi. Na stronie Mobipocket Developer Center znaleźć artykuły wraz z przykładami jak formacie mobi wygenerować słownik.

Każdy, kto jest zainteresowany, może pobrać wyżej opisany słownik kilkając w link.

wtorek, 11 maja 2010

PLINQ – sterowanie równoległością

Ostatnio opisywałem podstawowe informacje o PLINQ. Tym razem postaram się pokazać, jakie dodatkowe mechanizmy, oprócz tych znanych z “klasycznego” LINQ daje nam PLINQ.
Execution Modes
Przed uruchomieniem, PLINQ dokładnie analizuje zapytania, które będą wykonywane, aby sprawdzić, czy “opłaca” się “zrównoleglenie” zapytania. Ponieważ biblioteka nie jest w stanie sprawdzić, czy zapytanie szybciej działa równolegle czy szeregowo ani oszacować wielkości przetwarzanych danych, zapytanie sprawdzane jest pod kątem istnienia w nim metod, które z natury są sekwencyjne, takie jak: Skip, Take, TakeWhile, SkipWhile, czy indeksowane wersje Select i Where. Jeśli po sprawdzeniu wydajności wyszło nam, że lepiej jest jednak specyficzne zapytanie uruchomić na kilku wątkach, możemy wymusić użycie PLINQ, wywołując metodę WithExecutionMode z parametrem ParallelExecutionMode.ForceParallelism (jedyna sensowna opcja tego enuma), jak przykładzie poniżej:
var data = collection.AsParallel()
             .WithExecutionMode(
                 ParallelExecutionMode.ForceParallelism
              )
             .SkipWhile(el => ExpensiveCheck(el))
             .Select((el, idx) => Create(el, idx));
Przerywanie wywołania zapytań
Jak można się łatwo domyślić przerywanie wywołania PLINQ nie jest takie proste – przetwarzanie danych odbywa się w kilku wątkach, do których programista nie ma żadnego dostępu. Na szczęście projektanci biblioteki pomyśleli o i tym. Mechanizm stosowany do przerywania zapytań został wprowadzony w wersji 4 .NET Framework’a i jest wykorzystywany w całym przetwarzaniu równległym – nie tylko w bibliotece PLINQ (ale o tym kiedy indziej).
Przechodząc do szczegółów, klasa ParallelEnumerable oferuje metodę rozszerzającą WithCancellation, która jako parametr przyjmuje obiekt typu CancellationToken. Struktura ta służy do informowania o tym, że jakaś operacja ma być przerwana, choć sama nie ma żadnych metod, które pozwalają na anulowanie bieżącego przetwarzania. Do tego celu służy powiązana z nią klasa CancellationTokenSource. Aby zatem móc przerwać jakieś zapytanie PLINQ, musimy najpierw utworzyć instancję typu CancellationTokenSource, a następnie do metody WithCancellation przekazać property Token. Tak utworzone zapytanie możemy w każdej chwili przerwać wywołując metodę Cancel na wcześniej utworzonym obiekcie CancellationTokenSource. Spowoduje to przerwanie działania PLINQ i rzucenie wyjątku typu OperationCancelledException. Jak zwykle, przykład:
private CancellationTokenSource _cts = new CancellationTokenSource();

public List<T> OperationThread()
{
  
  try
  {
    var result = collection.AsParallel()
                   .WithCancellationToken(_cts.Token)
                   .Where(el => el.FulfilsCondition())
                   .Select(el => ExpensiveFunc(el));

    return result.ToList();
  }
  catch (OperationCanceledException e)
  {
    // Query was cancelled
  }
  return null;
}

public void CancelButtonClick()
{
  _cts.Cancel();
}
Kolejność elementów w kolekcji wynikowej
W “klasycznym” LINQ elementy w kolekcji wynikowej są w takiej samej kolejności jak zostały podane na wejściu. Takie podejście w przypadku stosowania “równoległości” wiązałoby się z dużymi kosztami – dlatego też, domyślnie PLINQ nie zachowuje kolejności elementów. Aby wymusić uporządkowanie elementów kolekcji na wyjściu, należy skorzystać z metody AsOrdered. Warto przy okazji wspomnieć, że metodę tę wolno używać tylko po wywołaniu jednej z 3 metod: AsParallel, Repeat oraz Range (w innym wypadku rzucony zostanie odpowiedni wyjątek). Jeśli w danym momencie chcemy przestać korzystać z uporządkowania elementów, wystarczy skorzystać z metody AsUnordered.
Inne możliwości
Biblioteka PLINQ oferuje nam jeszcze kilka innych opcji, o których warto tutaj wspomnieć:
  • Ustawienie liczby wątków, na których będzie wykonywane zapytanie: WithDegreeOfParallelism. Domyślnie, liczba wątków odpowiada liczbie procesorów na maszynie.
  • Ustalenie sposobu łączenia (merge) elementów w kolekcji wynikowej, czyli w jaki sposób zapytanie będzie buforowało wyniki z poszczególnych wątków – metoda WithMergeOptions przyjmująca jako parametr enumerację ParallelMergeOptions.
Linki
  1. PLINQ
  2. Order preservation in PLINQ
  3. How to: Cancel a PLINQ Query

niedziela, 9 maja 2010

PLINQ

Jak wiadomo, kilka tygodni temu wydano kolejną wersję .NET Framework’a a wraz z nią nowe Visual Studio. Zapoznając się z kolejnymi “feature’ami” czwartej już wersji Framework’a, postaram się napisać chociaż kilka zdań o tych najciekawszych.

Na pierwszy “ogień” pójdzie PLINQ. Cóż to zatem jest takiego? Otóż jest to nowa funkcjonalność polegająca na uruchamianiu zapytań LINQ dla obiektów równolegle (PLINQ = Parallel LINQ). Jak wiadomo obecnie producenci procesorów podążają w kierunku upakowania coraz większej ilości rdzeni na pojedynczym chipsecie, więc można przypuszczać, że w/w mechanizm szybko stanie popularny. Zwłaszcza, że użycie PLINQ (w najprostszym przypadku) jest banalne – sprowadza się ono do dopisania przed wywołaniem dowolnej metody “klasycznego” LINQ funkcji AsParallel().

Poniżej przykład:

var query = collection.AsParallel()
                .Select(el => el.ExpensiveMethod());

A teraz kilka słów jak to działa. Mianowicie do przestrzeni nazw System.Linq dodano nową klasę z metodami rozszerzającymi – ParallelEnumerable. Interfejs IEnumerable<T> rozszerza tylko o jedną metodę – wyżej już wspomnianą AsParallel(). Co ważne, metoda ta zwraca obiekt typu ParallelQuery<T>. I właśnie dla tej klasy metody rozszerzające znajdują się w klasie ParallelEnumerable – są tam wszystkie metody znane z “klasycznego” LINQ – jedyna różnica to zwracany obiekt – zamiast IEnumerable<T> mamy ParallelQuery<T>. Dzięki takiemu podejściu zrównoleglanie zapytań jest dziecinnie proste. Przykładowo mając zapytanie postaci:

var sum = collection.Where(el => el.IsCorrect())
                .Select(el => el.ExpensiveMethod())
                .Sum(el => el.Value);

zmiamy je w zapytanie wykonywane równoległe dopisując tylko np. na początku wywołania AsParallel():

var sum = collection.AsParallel()
                .Where(el => el.IsCorrect())
                .Select(el => el.ExpensiveMethod())
                .Sum(el => el.Value);

Często jest jednak tak, że nie całe zapytanie powinno być wykonywane równolegle – pewien fragment (ze względu np. na narzut na utworzenie wątków, itp.) może się wykonywać szybciej szeregowo. Aby zatem zrezygnować z dalszego wykonywania zapytania równolegle – ParallelEnumerable posiada specjalną metodę – AsSequential – po jej wywołaniu dalsza część zapytania będzie wykonywana szeregowo (metoda zwraca po prostu IEnumerable<T>, co powoduje, że dalsza część zapytania LINQ jest wykonywana “starym” sposobem). Przykładowo, mając następujące zapytanie:

var sum = collection.AsParallel()
                .Where(el => el.IsCorrect())
                .Select(el => el.ExpensiveMethod())
                .AsSequential()
                .FirstOrDefault(el => el.Value > 20);

Metody Where i Select zostaną wykonane równolegle, natomiast metoda FirstOrDefault będzie już wykonana sekwencyjnie.

To by było tyle wstępu o PLINQ, następnym razem postaram się opisać bardziej zaawansowane mechanizmy, czyli w jaki sposób sterować równoległym wykonywaniem zapytań.

Pierwszy post

No to zaczynamy. Postaram się (o ile oczywiście pozwoli na to czas) umieszczać na tym blogu  spostrzeżenia, artykuły, a także pewnie i różnorakie “potyczki” związane z technologiami M$, z którymi stykam się w czasie mojej pracy jako programista .NET. Blog ma głównie służyć jako zbiór notatek, do których w każdej chwili można wrócić, jeśli pojawi się jakiś już wcześniej rozwiązany problem, lub jeśli trzeba będzie ponownie skorzystać z dawno już nieużywanej technologii, czy biblioteki. Poza tym jak wiadomo, zapisanie czegoś zazwyczaj powoduje, że zostaje to dłużej w pamięci. 

Czy coś więcej z tego wyniknie, zobaczymy…