Category Archives: Wielowątkowość

Jak nie używać blokad lock

Słowo lock służy do synchronizacji  kodu między wątkami. Często jednak przeglądając kod widzę niepoprawne użycie. Rozważmy następujący przykład:

public class OrderManager
{
    public void Submit()
    {
        lock(this)
        {
            //...
        }
    }
}

Przedstawiona konstrukcja może spowodować wiele trudnych do wykrycia problemów. Co jeśli użytkownik naszej biblioteki również wykorzysta źle lock i napisze:

OrderManager orderManager=new OrderManager();
//...
lock(orderManager)
{
    orderManager.Submit();
}

Spowoduje to oczywiście deadlock’a. Pierwszy lock się uda (na orderManager), z kolei na drugim wykonanie się zakleszczy. Nie możemy wymagać od użytkowników naszego kodu, że wiedzą jakiego typu lock’i zaimplementowaliśmy. Musimy zawsze pisać kod, który będzie działać w jak największej liczbie przypadków użycia. Rozwiązanie jest proste:

public class OrderManager
{
    private object _sync=new object();
    
    public void Submit()
    {
        lock(_sync)
        {
            //...
        }
    }
}

Jeśli wykorzystujemy rzadko Lock ,warto dodać lazy loading dla _sync:

public class OrderManager
{
    private object _sync;
    
    public void Submit()
    {
        lock(GetSyncHandle())
        {
            //...
        }
    }
    private object GetSyncHandle()
    {
        System.Threading.Interlocked.CompareExchange(ref _sync, new object(), null);
          return _sync;
    }
}

CompareExchange gwarantuje atomowość operacji – dzięki temu wiemy, że dokładnie jedna kopia _sync zostanie utworzona.

Wielowątkowość: podstawowe pojęcia – deadlock, livelock, starvation.

O wielowątkowości pisałem już niejednokrotnie. Niestety w żadnym z moich postów, nie wyjaśniłem podstawowych pojęć związanych z współbieżnością. Oczywiście jeśli wykorzystujemy wątki do prostych zadań typu asynchroniczne połączenie z usługą, poważniejszych problemów nie doświadczymy. W przypadku jednak nieco bardziej zaawansowanych algorytmów, musimy zawsze badać nasz kod pod kątem:

1. Zakleszczenie (deadlock) – występuję gdy wątek A czeka aż wątek B skończy swoją operację a wątek B czeka aż wątek A zakończy akcję. W takiej sytuacji oczywiście algorytm nigdy nie skończy operacji, ponieważ wątki czekają na siebie nawzajem. Przykład w c#:

    private readonly object objectLockA = new object();
    private readonly object objectLockB = new object();
    
    public void MethodA()
    {
        lock(objectLockA)
        {
            lock(objectLockB)
            {        
            }
        }
    }
    
    public void MethodB()
    {
        lock(objectLockB)
        {
            lock(objectLockA)
            {
            }
        }
    }
    

Załóżmy, że MethodA oraz MethodB uruchamiane są w dwóch różnych wątkach.Ponadto przypuśćmy, że aktualnie obydwu metodom udało się wykonać pierwszy lock  – lock(objectLockA) dla metody A oraz lock(objectLockB) dla metody B. I co dalej? Oczywiście algorytm się zakleszczy ponieważ w przypadku metody A nie można będzie uzyskać blokady do objectLockB (została ona już przyznana metodzie B) a w przypadku metody B sytuacja wygląda analogicznie.

2. Zagłodzenie (starvation). Mamy kilka wątków i jeśli algorytm lub architektura pozwala na to, że jeden proces nigdy nie będzie miał szansy wykonania się, wtedy mówimy o zagłodzeniu procesu. W przypadku architektury systemu, powinna ona dopuścić do wykonania również wątki o najniższym priorytecie. Jeśli algorytm szeregowania procesów jest zły, wtedy istnieje szansa, że wątek zostanie zagłodzony, ponieważ zawsze będą wykonywane inne procesy np. o wyższym priorytecie. Problem również można rozwiązać na poziomie algorytmu. Można przecież tak zaprojektować algorytm,  że będzie on dopuszczał inne wątki do wykonania (np. poprzez wykonanie funkcji Sleep w określonych momentach). Sleep jednak jest najbardziej prymitywną metodą unikania zagłodzenia i powinno się problem rozwiązać poprzez poprawny przepływ między wątkami .

3. Livelock – stanowi specjalny przypadek zagłodzenia. Występuje gdy obydwa procesy aby uniknąć deadlock’a zatrzymują wykonywanie kodu aby dać szansę innym wątkom na wykonanie się. Aby ułatwić zrozumienie tego, wyobraźmy sobie sytuację gdy dwie osoby na korytarzu aby minąć siebie wybierają ciągle tą samą trasę, kończąc na ciągłej wzajemnej blokadzie. Livelock może wydawać się podobny do deadlock (rezultat jest taki sam). W przypadku Livelock stan procesu się jednak zmienia. Z kolei w deadlock, pozostaje ciągle taki sam. W przykładzie dwóch osób na korytarzu można zauważyć, że ciągle zmieniają swoją pozycje (lewo, prawo). Analizując deadlock i powyższy fragment kodu c#  można zauważyć, że stan pozostaje taki sam -  nic się nie dzieje, po prostu czekamy na akcję, nie zmieniając swojego stanu. Livelock jest częstym problemem w niepoprawnych algorytmach wykrywania i usuwania deadlock.

Single-threaded apartment oraz Multithreaded apartment.

Dziś trochę czystej teorii dla tych, którzy potrzebują wywoływać komponenty COM. W Internecie znajduje się wiele artykułów o różnicach między STA a MTA. Większość jednak opisuje je dosyć szczegółowo uwzględniając wiele aspektów technicznych i przez to nie zawsze może być to zrozumiałe. Podstawy jednak są bardzo proste i w poście skupie się wyłącznie na nich – szczegóły z pewnością znajdziecie na MSDN.

Przede wszystkim STA, MTA mają znaczenie wyłącznie gdy korzystamy z obiektów COM. To pozostałość po dawnych czasach. Na początku istnienia COM, nie było wielowątkowości i programiści nie musieli martwić się o synchronizację, sekcje krytyczne, semafory, muteksy, deadlock, livelock, zagłodzenie itp… Tworzone biblioteki nie wspierały więc wielowątkowości ponieważ takowa wtedy nie istniała. Problem pojawił się jednak gdy na rynku zaczęły pojawiać się pierwsze programy wielowątkowe. Wiadomo, że te stare biblioteki nadal będą musiały być wykorzystywane. Wprowadzono więc tzw. dwa modele STA oraz MTA.

Dla starych bibliotek używamy STA. W modelu przyjmuje się, że kod COM możemy być jednocześnie wywoływany z jednego wątku – co likwiduje nam problemy z synchronizacją itp. Jeśli zdarzy się, że 2 osobne wątki będą chciały wywołać dany kod, wtedy wywołania są szeregowane w kolejce (Windows Message Pump).

Z kolei w MTA dopuszcza się wywoływanie jednoczesne z różnych wątków. Biblioteki MTA muszę zatem być napisane w sposób “thread-safe”, czyli uwzględniającym wielowątkowość. Trudniejsze wyzwanie ale bardziej wydajne.

Wszystkie aplikacje WPF, WinForms wykorzystują biblioteki COM STA stąd ich wątek główny najczęściej oznaczony jest atrybutem STAThreadAttribute .

Singleton a wielowątkowość

Z racji tego, że w ostatnim czasie sporo pisałem o wielowątkowości w C#, dzisiaj pokaże prawidłową implementacje wzorca projektowego singleton przystosowanego do pracy w środowisku współbieżnym. Na początek przyjrzyjmy się klasycznej implementacji:

public sealed class Singleton
{
    private static Singleton m_Instance = null;
    
    private Singleton() { }
    
    public static Singleton Instance
    {
        get
        {
            if(m_Instance == null)
                m_Instance = new Signleton();
            return m_Instance;
        }
    }
}

Wyobraźmy sobie sytuację w której dwa wątki jednocześnie próbują utworzyć instancję klasy. Może zdarzyć się, że wątki jednocześnie wejdą do if’a sprawdzającego m_Instance. W takim przypadku zostaną utworzone dwie instancje Signleton. Jak temu zaradzić?

Wystarczy użyć jednego z mechanizmów synchronizacji opisanych we wcześniejszych postach. Polecam lock – w przypadku synchronizacji wewnątrz AppDomain jest to przeważnie najlepsze rozwiązanie.

public sealed class Singleton
{
    private static Singleton m_Instance = null;
    private static readonly object m_Sync = new object();
    
    private Singleton() { }
    
    public static Singleton Instance
    {
        get
        {
            lock(m_Sync)
            {
                if(m_Instance == null)
                    m_Instance = new Signleton();
            }
            return m_Instance;
        }
    }
}

Powyższa implementacja jest poprawna, jednak ma jedną wadę – za każdym razem trzeba zakładać blokadę co w środowisku rozproszonym może spowodować istotny spadek wydajności. Spróbujmy więc zakładać blokadę wyłącznie gdy m_Instance jest równy null (czyli podczas pierwszego wywołania właściwości Instance):

public sealed class Singleton
{
    private static Singleton m_Instance = null;
    private static readonly object m_Sync = new object();
    
    private Singleton() { }
    
    public static Singleton Instance
    {
        get
        {
            if(m_Instance == null )
            {
                lock(m_Sync)
                {
                    if(m_Instance == null)
                        m_Instance = new Signleton();
                }
            }
            return m_Instance;
        }
    }
}

Z kodu widzimy, że synchronizujemy kod tylko gdy instancja nie została jeszcze utworzona. Po zainicjowaniu obiektu kod działa już w pełni optymalnie, zwracając po prostu obiekt Singleton.

Jeśli nie zależy nam na tzw. lazy loading(opóźnione ładowanie) możemy stworzyć instancję obiektu w momencie deklaracji m_Instance:

public sealed class Singleton
{
    private static Singleton m_Instance = new Singleton();
    
    private Singleton() { }
    
    public static Singleton Instance
    {
        get
        {            
            return m_Instance;
        }
    }
}

Implementacja jest w pełni optymalna oraz bezpieczna w środowisku współbieżnym. Jedyną wadą jest fakt, że Signleton będzie utworzony(podczas np. dostępu do innych statycznych pól klasy) nawet w przypadku gdy  z niego nie będziemy korzystać w programie. Jeśli wiemy, że za każdym razem podczas działania aplikacji będziemy wywoływać klasę to powyższe rozwiązanie może okazać się bardzo dobre.

Czas na najlepsze moim zdaniem rozwiązanie:

public sealed class Singleton
{   
   private Singleton() { }

   public static Singleton Instance
   {
       get
       {
           return Child.m_Instance;
       }
   }
   public static int TestValue { get; set; }
   class Child
   {
       private Child() { }
       internal static readonly Singleton m_Instance = new Singleton();
   }        
}

Przedstawiona implementacja jest optymalna ponieważ nie zakładamy żadnych blokad. Ponadto jest w pełni “leniwa”- inicjujemy obiekt dopiero gdy chcemy mieć do niego dostęp. Jeśli przypiszemy jakąś wartość właściwości TestValue, obiekt Singleton  nie zostanie utworzony – w przypadku wcześniejszej implementacji jakikolwiek dostęp do statycznych właściwośći powodował inicjalizację Singleton. Rozwiązanie jest oczywiście bezpieczne w środowisku współbieżnym.

Wielowątkowość(pętle, Task) w C# 4.0

Programowanie współbieżne w c# 4.0 jest znacznie łatwiejsze w porównaniu z poprzednią wersją. Widać, że platforma .NET staje się coraz dogodniejszym środowiskiem programistycznym dla rozwiązań równoległych.

Zacznijmy od pętli foreach. Przeważnie wykonujemy ją w sposób sekwencyjny. Jeśli chcielibyśmy zrównoleglić ją, musielibyśmy stworzyć instancję Thread i zawartość pętli umieścić w wątkach. Ponadto proces wymagałby użycia np. semafora albo ManualResetEvent aby zsynchronizować kod wykonywany po zakończeniu pętli. Na szczęście w wersji 4.0 mamy do dyspozycji gotowy mechanizm:

string []tab=new string[]{"a","b","c","d","e"};
System.Threading.Tasks.Parallel.ForEach<string>(tab, Method);

gdzie Method:

private static void Method(string val)
{
}

Pętla zostanie wykonana w sposób równoległy. Jeśli tylko nie współdzielimy jakiego stanu pomiędzy kolejnymi iteracjami, warto zastanowić się nad tym typem pętli,  ponieważ jest to bardzo proste a na procesorach wielordzeniowych wzrost wydajności może być znaczący.

Nową klasą w C# 4.0 jest System.Threading.Tasks.Task. Umożliwia ona wykonanie operacji w tle(w osobnym wątku). Warto podkreślić, że Task bazuje na ulepszonym ThreadPool, zatem wydajność tworzenia nowego Task’a jest dużo wyższa niż w przypadku “ciężkiego” Thread. Samo stworzenie Task’a jest analogiczne do Thread:

  Task t1 = new Task(() => Console.Write("task 1"));
  t1.Start();    

Możemy również poczekać aż task zostanie wykonany:

Task t1 = new Task(() => Console.Write("task 1"));
t1.Start();
t1.Wait();

Ciekawszym jednak rozwiązaniem jest tzw. kontynuacja zadań. Możemy stworzyć listę zadań, które będą wykonywały się po kolei. Często mamy przecież problemy współbieżne w których wykonanie kolejnego etapu musi być poprzedzone zakończeniem poprzedniego. Za pomocą ContinueWith możemy w łatwy sposób stworzyć taki plan wykonania:

Task t1 = new Task(delegate()
 {                    
     Console.Write("task 1");
     Thread.Sleep(5000);
 }
);
Task t2 = t1.ContinueWith((t) => Console.Write("task 2"));
t1.Start();
t2.Wait();

Powyższy kod gwarantuje, że zadanie t2 zostanie wykonane dopiero po zakończeniu t1.

Synchronizacja wątków(AutoResetEvent, ManualResetEvent, Interlocked), część 3

W celu synchronizacji wątków można wykorzystać mechanizm zdarzeń: ManualResetEvent oraz AutoResetEvent. Rozwiązanie polega na zastosowaniu sygnalizacji. Chcąc wejść do sekcji krytycznej piszemy:

ManualResetEvent resetEvent = new ManualResetEvent(false);
resetEvent.WaitOne();

W konstruktorze ustawiamy początkową wartość sygnału na false(brak sygnału). Następnie wywołujemy metodę WaitOne, która czeka na nadejście sygnału. Metoda blokuje kod aż  do momentu gdy w jakimś miejscu kodu zostanie wysłane zdarzenie za pomocą metody Set:

resetEvent.Set();

AutoResetEvent działa bardzo podobnie – jedyną różnicą jest fakt, że po otrzymaniu sygnału w WaitOne, stan zostaje ponownie ustawiony na false. Rozważmy dwa fragmenty kodu z ManualResetEvent oraz AutoResetEvent:

ManualResetEvent resetEvent = new ManualResetEvent(true);
resetEvent.WaitOne();
resetEvent.WaitOne();
MessageBox.Show("Hello world"); // zostanie wykonane

AutoResetEvent resetEvent = new AutoResetEvent(true);
resetEvent.WaitOne();            
resetEvent.WaitOne();
MessageBox.Show("Hello world"); // nigdy nie zostanie wykonane

W przypadku ManualResetEvent, WaitOne sprawdza tylko sygnał i nie modyfikuje go. Zatem wiadomość MessageBox zostanie wyświetlona. W przypadku AutoResetEvent, kod zostanie zablokowany na drugim wywołaniu WaitOne ponieważ  pierwsze wykonanie WaitOne ustawi sygnał na false.

Interlocked jest bardzo wydajnym narzędziem(statyczną klasą) umożliwiającym wykonywanie podstawowych operacji takich jak inkrementacja czy zamiana dwóch liczb w sposób atomowy czyli bezpieczny z punktu widzenia współbieżności. Konkretnie do dyspozycji mamy następujące metody:

Add

Dodanie dwóch liczb ze sobą.

CompareExchange

Porównanie ze sobą dwóch liczb i w przypadku gdy są one równe zastąpienie pierwszej z nich wartością dostarczoną w drugim parametrze.

Decrement

Zmniejszenie wartości o jeden.

Exchange

Zamiana wartości zmiennych.

Increment

Inkrementacja(zwiększenie o jeden).

Read

Przydatne na procesorach 32 bitowych gdzie operacje czytania zmiennych 64-bitowych nie jest atomowa.

Przykład użycia:

int value = 5;
Interlocked.Increment(ref value);
System.Diagnostics.Debug.Assert(value == 6);

W następnej, ostatniej części napiszę o kilku  nowościach w .NET 4.0, zapraszam.

Synchronizacja wątków(semafor, mutex), część 2.

W dzisiejszym poście przedstawię zasadę działania semafora oraz mutexa. Zacznijmy od teorii, czym jest semafor i jak można go zaimplementować? Otóż semafor jest sposobem na realizację wzajemnego wykluczania – zapewnienia, że tylko określona liczba wątków będzie mogła jednocześnie wykonać dany fragment kodu. Wyróżniamy semafory binarne, które dopuszczają maksymalnie jeden wątek oraz semafory ogólne, które umożliwiają jednoczesny dostęp określoną przez programistę liczbę wątków.

Implementacja semafora wymaga zaprogramowania dwóch metod: wait oraz signal. Programista wchodząc do sekcji krytycznej wywołuję wait a wychodząc wykonuje signal. Zatem kod źródłowy semafora ogólnego mógłby wyglądać następująco:

// ustawienie wartośći początkowej semafora
counter=5;

// metody atomowe
void Wait()
{
    while(counter<=0){}
    counter--;
}
void Signal()
{
    counter++;
}

Jeśli zasoby aktualnie są używane, metoda Wait będzie blokować dalsze wykonywanie kodu. Dopiero w momencie gdy sekcja krytyczna zostanie opuszczona(wywołanie Signal), warunek w Wait zostanie spełniony i dalszy kod będzie mógł się wykonać. Należy zaznaczyć, że metody muszą być atomowe ponieważ w przeciwnym razie mogą wystąpić problemy synchronizacyjne opisane w poprzednim poście. W przypadku gdy counter posiada wartość początkową równą 1, mamy do czynienia z semaforem binarnym.

Powyższy kod pokazałem tylko po to aby rozjaśnić sposób działania semafora. C# posiada bowiem  gotową klasę System.Threading.Semaphore:

System.Threading.Semaphore semaphore = new System.Threading.Semaphore(1, 1);
semaphore.WaitOne();
// sekcja krytyczna
semaphore.Release(1);

Konstruktor przyjmuje kolejno aktualną oraz maksymalną wartość licznika counter.

Przejdźmy teraz do następnego mechanizmu synchronizacji – Mutex’a. Mutex od strony użytkownika wygląda bardzo podobnie do semafora. Umożliwia jednak synchronizację na poziomie procesów a nie tylko wewnątrz AppDomain.

Należy również wspomnieć o zasadzie  “principle of ownership” – tylko wątek który nałożył blokadę może ją później zdjąć. W przeciwieństwie do semaforów, nie możemy ustawić blokady w wątku A a zdjąć jej w wątku B – zakończy się to wyrzuceniem wyjątku.

Klasycznym przykładem wykorzystania mutexów jest zapewnienie, że tylko jedna instancja programu zostanie uruchomiona:

class Program 
{ 
    static void Main(string[] args) 
    { 
        Mutex oneMutex = null; 

        const string MutexName = "SingleInstance";
        try 
        {         
            oneMutex = Mutex.OpenExisting(MutexName); 
        } 
        catch (WaitHandleCannotBeOpenedException) 
        { 
            // Mutex nie istnieje, obsługa wyjątku
        }         
        if (oneMutex == null)         
        {         
            oneMutex = new Mutex(true, MutexName); 
        } 
        else 
        {         
            oneMutex.Close(); 
            return; 
        } 
        // tworzenie okna itp.
    } 
}

Z przykładu widać, że podczas tworzenia obiektu Mutex można przekazać jego nazwę, która służyć będzie do rozpoznawania obiektu w różnych procesach. W przypadku gdy funkcja OpenExisting zwróci wartość różną od NULL(Mutex już utworzony), program zakończy działanie ponieważ zostanie wykonana instrukcja “return;”.

Oczywiście Mutex posiada również metody WaitOne oraz ReleaseMutex. Sposób użytkowania jest niemalże identyczny jak w przypadku semafora binarnego, więc nie będę pokazywał już kodu źródłowego.

W następnym poście planuje opisać klasy ManualResetEvent, AutoResetEvent oraz Interlocked – zapraszam do odwiedzenia bloga za kilka dni.

Synchronizacja wątków w C# (lock, Monitor), część 1

Najtrudniejszym zadaniem w programowaniu współbieżnym jest programowanie sekwencyjne a uściślając synchronizacja wątków;). Pewne operacje w naszych programach muszą być wykonywane w sposób sekwencyjny. Często dostęp do danych współdzielonych nie może odbywać się w sposób równoległy. Rozważmy klasyczny problem zwiększania liczby o jeden:

counter = counter + 1;

Jeśli zmienna counter jest współdzielona przez kilka wątków, powyższa  operacja jest niepoprawna. Dlaczego? Zacznijmy od początku. Zwiększanie liczby o jeden, tak naprawdę składa się z trzech operacji:

  1. Wczytanie zmiennej do rejestru procesora.
  2. Zwiększenie wartości w rejestrze o jeden.
  3. Przekopiowanie wartości rejestru z powrotem do zmiennej.

Załóżmy, że counter ma wartość 10. Wątek T1 chce zwiększyć wartość i wczytuje ją do rejestru. Następnie wątek T2 również chce zwiększyć wartość i kopiuje ją do rejestru(wciąż counter==10), zwiększa o jeden i przenosi wynik z powrotem do zmiennej. Zmienna zatem w tej chwili ma już wartość 11. Wątek T1 jednak w swoim rejestrze ma nieaktualną wartość 10. Bez synchronizacji, wątek T1 zwiększy wartość rejestru o 1 i otrzyma nieprawidłowy wynik 11(powinno być 12). Jeśli zamotałem, to mam nadzieję, że poniższy pseudokod rozjaśni problem:

;Wątek T1
Time1:    Kopiowanie wartości do rejestru.
Time4:    Zwiększenie rejestru o jeden. ;rejestr w tej chwili ma już nieaktualną wartość.
Time5:    Przekopiowanie rejestru do zmiennej counter.

;Wątek T2
Time1:    Kopiowanie wartości do rejestru.
Time2:    Zwiększenie rejestru o jeden.
Time3:    Przekopiowanie rejestru do zmiennej counter.

W programowaniu równoległym nie można zakładać, że wątki pracują w tym samym tempem. Każda operacja może trwać dłużej lub krócej w zależności od rdzenia procesora, priorytetu itp.

Zatem wszelkie operacje podzielne powinny być wykonywane w specjalnych obszarach kodu do których dostęp ma tylko jeden wątek jednocześnie –  tzw. sekcjach krytycznych.

C# posiada wiele mechanizmów synchronizacji. Zacznijmy od najpopularniejszego – operatora lock:

class Example
{
    private object m_SyncObject=new object();
    public void ThreadMethod()
    {    
        lock(m_SyncObject)
        {
            // sekcja krytyczna
        }
    }
}

Jak widać pewnie  ułatwienie dla synchronizacji posiada już sam język C# dostarczając słowo kluczowe lock. Niezbędnym jest dostarczenie obiektu, który stanowi pewne odniesienie dla synchronizacji. Za pomocą operatora lock blokujemy dostęp do dostarczonego obiektu (w tym przypadku m_SyncObject). Jeśli drugi wątek będzie chciał w tym samym momencie wejść do obszaru lock, będzie musiał poczekać aż pierwszy skończy wszelkie operacje zawarte w klauzuli lock. Wejście do obszaru lock z tym samym obiektem synchronizującym (m_SyncObject) możliwe jest wyłącznie przez jeden wątek .

Warto zaznaczyć, że nie powinno się używać referencji this oraz operatora lock:

class Example
{    
    virtual public void ThreadMethod()
    {    
        lock(this)
        {
            // sekcja krytyczna
        }
    }
}

Powyższy kod wywoła blokadę (deadlock)  w sytuacji gdy użytkownik klasy napiszę np.:

Example example=new Example();
lock(example)
{
    example.ThreadMethod();
}

Pierwszy lock(example) zadziała, jednak następny w ThreadMethod już nie zostanie nadany ponieważ referencje this oraz example odnoszą się do tego samego obiektu i tym samym lock(this) będzie czekał aż lock(example) zostanie zwolniony(co nie nastąpi nigdy).

Projektując biblioteki nie możemy przewidzieć w jaki sposób przyszli użytkownicy będą korzystać z naszej klasy więc powinniśmy pisać w sposób jak najbardziej elastyczny.

Następnym mechanizmem jest klasa Monitor. W rzeczywistości jest ona tym samym co lock. Słowo kluczowe lock  zostało wprowadzone po to aby programiści nie musieli pisać poniższego kodu:

class Example
{
   private object m_SyncObject = new object();
   public void ThreadMethod()
   {
       System.Threading.Monitor.Enter(m_SyncObject);
       try
       {
           // sekcja krytyczna
       }
       finally
       {
           System.Threading.Monitor.Exit(m_SyncObject);
       }
   }
}

Zwróćcie uwagę na try i finally. Bez tego awaria sekcji krytycznej(wyjątek) mogłaby spodować błędne zakończenie(brak wywołania metody Exit). Polecam wszystkim jednak używanie lock ponieważ trudniej w nim o popełnienie błędu takiego jak np. brak wywołania Exit. Warto jednak przyjrzeć się metodzie Monitor.TryEnter, która pozwala na sprawdzenie czy wejście do sekcji krytycznej jest możliwe.

W następnych postach planuje opisanie klas Mutex, Semaphore, AutoResetEvent, ManualResetEvent, Interlocked oraz o kilku nowościach wprowadzonych w .NET 4.0.

Wydajność wątków w C#

W języku C# mamy kilka mechanizmów tworzenia wątków. Różnią się one zarówno wydajnością jak i przeznaczeniem.

Zacznijmy więc od najpopularniejszego sposobu a mianowicie klasy System.Threading.Thread. Stworzenie wątku polega na inicjalizacji klasy oraz wywołania metody Start:

public class ThreadExample
{
    public CreateThread()
    {            
        System.Threading.Thread thread = new System.Threading.Thread(ThreadMethod);
        thread.Start(null);
    }
    private void ThreadMethod(object parameters)
    {
        while(true)
        {
            // jakiś kod
        }
    }
}

ThreadMethod zawiera kod, który chcemy wykonać współbieżnie. Warto także wspomnieć o właściwości Thread.IsBackground. Gdy ustawiamy ją na true(domyślnie jest false), wtedy wątek stanie się tzw. “background thread” – wątkiem zależnym od rodzica. W sytuacji gdy użytkownik zamknie naszą aplikację, automatycznie zostaną zamknięte wszystkie wątki “background”. Gdybyśmy nie ustawili IsBackground na true, po zamknięciu aplikacji, stworzone przez nas wątki nadal by pracowały, uniemożliwiając tym samym prawidłowe zamknięcie programu.

Najważniejszą jednak rzeczą, którą chciałem poruszyć w poście jest wydajność. Tworzenie klas Thread jest bardzo czasochłonne. O ile nie piszemy programu z naprawdę dużą ilością wątków, to podejście polegające na użyciu klasy Thread jest jednym z najgorszych rozwiązań. Związane jest to z długim czasem wymaganym na inicjalizacje tej klasy. Użycie Thread jest dobre gdy tworzymy pule wątków, które działają przez cały czas działania aplikacji.

Gdy tworzymy często nowe wątki, znacznie lepszym rozwiązaniem jest skorzystanie z gotowej puli wątków, które czekają już zainicjowane na użycie:

public class ThreadPoolExample
{
    public ThreadPoolExample()
    {        
        System.Threading.ThreadPool.QueueUserWorkItem(ThreadPoolMethod);
    }
    private void ThreadPoolMethod(object state) 
    {
        while(true)
        {
            // jakiś kod
        }
    }
}

Wywołując metodę QueueUserWorkItem system sprawdza czy w puli wątków jest wolny wątek .Jeśli tak, zwraca nam go i przechodzi do wykonywania kodu współbieżnego. Wątki zatem nie są tworzone za każdym razem od nowa – zawsze zwracane nam są już gotowe wątki do użycia. Gdy skończymy wykonywanie kodu współbieżnego, wątek nie zostaje zniszczony a wyłącznie zwrócony z powrotem do puli.

Dodatkowo warto wspomnieć, że wszystkie wątki z puli są typu “background”.

Gdy tworzymy interfejs użytkownika i chcemy jakaś operację wykonać w tle(np. połączenie się z WCF) warto zastanowić się nad klasą  BackgroundWorker. Przede wszystkim jest ona bardzo łatwo w użyciu(można ją nawet przeciągnąć z toolbox’a jako zwykłą kontrolkę) oraz posiada kilka zdarzeń, które są istotne dla GUI – np. powiadomienie o postępie prac. Krótki przykład:

public partial class Form1 : Form
{
   public Form1()
   {
       InitializeComponent();
       BackgroundWorker backgroundWorker = new BackgroundWorker();
       backgroundWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker_RunWorkerCompleted);
       backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
       backgroundWorker.RunWorkerAsync();
   }

   void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
   {
       // e.ProgressPercentage
   }

   void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
   {
       while (true)
       {
           // jakiś kod kod
       }
   }        
}

Jeśli chcemy wykonywać pewną operacje co jakiś czas np. aktualizację lokalnej bazy danych z danymi otrzymywanymi przez usługę sieciową warto użyć timer’a:

public partial class Form1 : Form
{
   public Form1()
   {
       InitializeComponent();
       System.Threading.Timer timer = new System.Threading.Timer(ThreadMethod,null,0,1000);
       
   }
   private void ThreadMethod(object state)
   {
        // jakiś kod
   }
}

Jak widać ten specjalny timer znajduje się w przestrzeni nazw System.Threading. Metoda ThreadMethod będzie wykonywana współbieżnie co określony czas, przekazany w ostatnim parametrze konstruktora.

Aktualizacja interfejsu z drugiego wątku(windows forms i WPF)

Na różnych forach często użytkownicy mają problem z aktualizacją kontrolek z innego wątku. Załóżmy, że odpaliliśmy sobie BackgroundWorker lub po prostu Thread. Wykonujemy jakieś operację, np. łączymy się ze zdalnymi zasobami. Stworzenie osobnego wątku w takim scenariuszu jest bardzo pożądane ponieważ nie blokujemy wtedy aktualizacji interfejsu. W trakcie pobierania informacji z Internetu chcemy aktualizować interfejs aby informować użytkownika o postępach np.

progressBar.Value = progessValue;

Jeśli powyższy kod jest wywołany z obcego wątku użytkownik dostanie następujący komunikat o błędzie:

Cross-thread operation not valid:
Control accessed from a thread other than the thread it was created on.

Wyjątek jest wyrzucany ponieważ zarówno w WindowsForms jak i w WPF nie można aktualizować interfejsu z innego wątku niż z tego w którym została stworzona kontrolka. Musimy więc w jakiś sposób dostać się do wątku macierzystego dla kontrolki i tam wykonać aktualizacje tej kontrolki. Służy do tego metoda Control.Invoke(windows forms) lub ControlDispatcher.Invoke(Wpf). Chcąc więc zaktualizować ProgressBar w WinForms musimy napisać:

Action<int> updateAction = new Action<int>((value) => progressBar.Value = value);
progressBar.Invoke(updateAction,32);

Powyższy kod będzie działał w każdym przypadku ale ma jedną wadę związaną z wydajnością kodu. Co w przypadku gdy mamy osobną klasę do aktualizacji interfejsu i jest ona wywoływana zarówno z wątku kontrolki jak i z obcego wątku? Dla drugiego przypadku(z obcego wątku) kod jest maksymalnie optymalny. Z kolei w sytuacji gdy wywołujemy ją z wątku macierzystego dla kontrolki nie potrzebnie będzie wykonywana masa operacji związanych z wpompowaniem operacji w kolejkę dla wątku interfejsu. Na szczęście istnieje gotowa metoda, która stwierdza czy dla aktualnego przypadku użycia jest wymagane wywołanie Invoke:

Action<int> updateAction = new Action<int>((value) => progressBar.Value = value);
if (progressBar.InvokeRequired)
    progressBar.Invoke(updateAction,5);
else
    updateAction(4);

Posiadamy już optymalną wersje kodu. Jednak pisanie powyższego kodu za każdym razem gdy chcemy zaktualizować interfejs(nigdy nie wiemy czy kod nie będzie wywoływany w przyszłości z innego wątku) jest co najmniej niewygodne. Z ratunkiem przychodzą nam tzw. rozszerzenia(extensions,c# 3.0). Możemy przecież stworzyć własną metodę nazwaną powiedzmy InvokeIfRequired:

public static class ControlExtensions
{
   public static void InvokeIfRequired(this Control control, Action action)
   {
       if (control.InvokeRequired)
           control.Invoke(action);
       else
           action();
   }
   public static void InvokeIfRequired<T>(this Control control, Action<T> action, T parameter)
   {
       if (control.InvokeRequired)
           control.Invoke(action, parameter);
       else
           action(parameter);
   }
}

Za pomocą takiego rozwiązania aktualizacja kontrolki sprowadzi się wyłącznie do poniższego kodu:

this.InvokeIfRequired((value) => progressBar.Value = value, 10);

W przypadku Wpf rozwiązanie jest bardzo podobne:

public static class ControlExtensions
{
   public static void InvokeIfRequired(this Control control, Action action)
   {
       if (System.Threading.Thread.CurrentThread!=control.Dispatcher.Thread)
           control.Dispatcher.Invoke(action);
       else
           action();
   }
   public static void InvokeIfRequired<T>(this Control control, Action<T> action, T parameter)
   {
       if (System.Threading.Thread.CurrentThread!=control.Dispatcher.Thread)
           control.Dispatcher.Invoke(action, parameter);
       else
           action(parameter);
   }
}

Z kolei użycie metody jest identyczne jak w przypadku WinForms:

this.InvokeIfRequired((value) => bar.Value = value, 10);