Udostępnij za pośrednictwem


          

Dobre i złe praktyki w C# - część III (programowanie współbieżne)  

Udostępnij na: Facebook

Autor: Piotr Zieliński

Opublikowano: 2012-08-13

Wprowadzenie

W poprzednim artykule (część II) zostały przedstawione praktyki związane z programowaniem współbieżnym. Tematyka programowania równoległego jest na tyle obszerna, że jedna cześć nie wystarczyła, aby opisać nawet najczęściej popełniane błędy. W tym artykule nastąpi kontynuacja dobrych i złych praktyk, a w szczególności pisania kodu współbieżnego, bez użycia czasochłonnych blokad LOCK.

Unikanie blokad - alternatywne sposoby synchronizacji. Wstęp

Słowo kluczowe LOCK jest bez wątpienia najczęściej wybieranym mechanizmem synchronizacji. W wielu przypadkach jest to najlepsze rozwiązanie, a przynajmniej najbezpieczniejsze. Przedstawione poniżej mechanizmy należy stosować po dokładnym przeanalizowaniu danego scenariusza. Użycie ich w błędnym przypadku może spowodować spadek wydajności i wysokie obciążenie procesora. Jeśli nie ma pewności, które rozwiązanie jest wydajniejsze, wtedy lepiej użyć po prostu tradycyjnego LOCKA i nie ryzykować.

W poprzednim artykule, jako pierwszą alternatywę przedstawiono napisanie algorytmu w taki sposób, że jest on zawsze bezpieczny z punktu widzenia wielowątkowości. Zdecydowanie jest to najlepszy i najwydajniejszy sposób, jednak nie zawsze wykonalny. Jeśli nie da się napisać algorytmu inaczej, wtedy należy pomyśleć o innych sposobach synchronizacji. Najpierw jednak, trzeba dowiedzieć się, co tak naprawdę robi LOCK i dlaczego jest taki zły. Jeśli LOCK został założony w wątku A, a wątek B również próbuje uzyskać dostęp do tych samych zasobów, wtedy jest on usypiany. Słowo LOCK usypia wątek, gdy tylko nie jest możliwe uzyskanie dostępu. Ma to zalety takie, że uśpiony wątek nie blokuje CPU, a zasoby mogą być przeznaczone do uruchomienia innego wątku. W momencie, gdy wątek A zdejmuje blokady, wtedy wątek B jest ponownie budzony. Wszystko byłoby perfekcyjne, gdyby nie fakt, że uśpienie i przebudzenie jest procesem dość skomplikowanym. Dlaczego?

Przede wszystkim musi nastąpić tzw. przełączenie kontekstu (context switch). Przełączenie kontekstu to po prostu załadowanie odpowiednich danych do rejestru procesora. Jeśli wątek jest wznawiany, wtedy muszą zostać załadowane odpowiednie rejestry. Innymi słowy jest to po prostu deserializacja wątku (w dużym uproszczeniu oczywiście). Oprócz zmiany kontekstu musi nastąpić tzw. task scheduling, czyli szeregowanie zadań. Implementacja różni się w zależności od systemu operacyjnego, jednak zawsze jest to dodatkowa operacja do wykonania. Należy zdać sobie sprawę, że blokada powoduje opóźnienia w wykonywaniu kodu, a wiele operacji odbywa się w tle (context switching, task scheduling). Składnia C# jest bardzo prosta, składa się tylko z jednego słowa kluczowego. W praktyce to jedno słowo kluczowe wykonuje skomplikowane i czasochłonne operacje.

Jakie są zatem alternatywy? Oprócz blokad w świecie wielowątkowym, istnieje mechanizm zwany spinning (spinlock).  Mechanizm jest bardzo prosty – to zwykła pętla, sprawdzająca, czy można uzyskać dostęp do sekcji krytycznej. Jeśli żaden inny wątek nie korzysta z danego kodu, wtedy dostęp jest od razu nadawany. W przeciwnym wypadku pętla po prostu „wiruje”, czekając, aż inny wątek zwolni dostęp. W pseudokodzie można zapisać to następująco:

while (IsAccessDenied)
{
    // spinning (“wirowanie”)
}

W spinningu nie ma żadnego przełączania procesu, ponieważ wątek nie jest usypiany. Pętla będzie „wirować”, aż do momentu uzyskania dostępu. Oczywiście rozwiązanie ma pewne wady. Jeśli komputer posiada wyłącznie jeden procesor z pojedynczym rdzeniem, wtedy spinning po prostu spowoduje wysokie zużycie procesora. Zbyt długie „wirowanie” również pożera zbyt dużo jego zasobów. W sytuacjach, w których jest pewność, że dostęp zostanie zwolniony po kilku sekundach, spinning okazuje się jednak dużo bardziej wydajny niż blokada. Nie ma sensu usypiać wątku, jeśli wiadomo, że za chwilę dostęp zostanie nadany. Z kolei, blokady powinny być używane w sytuacjach, w których nie wiadomo albo nie ma się pewności, że dostęp został zabroniony na krótki czas.

Unikanie blokad – struktura SpinLock

SpinLock to jedna z klas, implementująca opisany wyżej mechanizm spinningu. Sposób wykorzystania jest prosty i sprowadza się do wywołania metody Enter oraz Exit, kolejno na początku i na końcu sekcji krytycznej:

internal class Program
{
private static int _counter;
private static SpinLock _spinLock=new SpinLock();

private static void Main(string[] args)
{
  for (int i = 0; i < 10000; i++)
  {
      var task = new Task(IncreaseValue);
      task.Start();
  }
  Thread.Sleep(7000);
  Console.WriteLine(_counter);
}
private static void IncreaseValue()
{
  bool lockTaken = false;

  try
  {
      _spinLock.Enter(ref lockTaken);
      _counter++;
  }
  finally
  {
      if (lockTaken)
          _spinLock.Exit();
  }
 }
}

Zaglądając do dokumentacji MSDN można przekonać się, że SpinLock to struktura, a nie klasa. Przez to przekazywanie SpinLock'a jako parametr do metody spowoduje wykonanie kopii i tym samym zakłócenie synchronizacji.

Flaga lockTaken, przekazywana do metody Enter, będzie zawierać wartość określającą, czy udało się uzyskać dostęp do sekcji krytycznej. Zgodnie z MSDN, należy najpierw ustawić tę flagę na false (przed przekazaniem do metody).

Ostatnią sugestią jest wewnętrzna implementacja metody Enter. Nie można jej wywoływać wielokrotnie z tego samego wątku, jeśli w międzyczasie nie została wywołana Exit. Dwukrotne wywołanie Enter w tym samym wątku spowoduje wyrzucenie wyjątku lub zakleszczenie, w zależności od IsThreadOwnerTrackingEnabled.

We wstępie teoretycznym zostało wspomniane, że spinning jest stanowczo odradzany dla operacji czasochłonnych. Niestety, nawet jeśli operacja jest krótka i jej wykonanie nie powinno zająć dużo czasu, nie ma pewności, czy system operacyjny nie uśpi danego wątku, na który właśnie spinning czeka.

Unikanie przełączania kontekstu – Thread.SpinWait zamiast Thread.Sleep

Thread.Sleep to bez wątpienia najbardziej znany sposób uśpienia wątku. Podobnie jak słowo LOCK, w wielu przypadkach jest to najlepsze i najbezpieczniejsze rozwiązanie. Sposób wykorzystania SpinWait jest bardzo podobny do Thread.Sleep, tj.:

Thread.SpinWait(100000000)

Parametr wejściowy SpinWait to nie liczba milisekund (jak to w przypadku Thread.Sleep), a liczba tzw. iteracji. Czym jest liczba iteracji? Niestety, dokumentacja MSDN nie dostarcza potrzebnego opisu, ale osobiście uważam, że jest to liczba wykonanych cykli lub operacji, a co za tym idzie, zależne jest to od wewnętrznej budowy procesora. SpinWait nie usypia wątku, w przeciwieństwie do Thread.Sleep, zatem nie powoduje zmiany kontekstu. Jeśli liczba iteracji nie jest zbyt duża, wtedy lepiej po prostu przeczekać, niż usypiać wątek, i wykonać te wszystkie skomplikowane operacje, które zostały opisane wyżej. Przeznaczone jednak jest to dla zaawansowanych algorytmów, których specyfika jest bardzo dobrze znana autorowi.

Thread.Yield, Thread.Sleep(1) oraz Thread.Sleep(0)

Funkcja Sleep jest dobrze znana, jednak mimo wszystko jej wewnętrzna budowa, a tym samym zachowanie, nie jest zwykle prawidłowo interpretowana. Z kolei, Thread.Yield jest jeszcze mniej znana…

Thread.Yield to nic innego, jak ustąpienie miejsca innemu wątkowi. Jeśli wątek A wywołuje Thread.Yield, wtedy system operacyjny ma za zadanie wykonanie ponownego szeregowania i przyznanie zasobów procesora innemu wątku. Wątek A ponownie zostanie wznowiony w momencie, gdy przyjdzie na niego kolej. Yield to wyłącznie ustąpienie miejsca, a nie całkowita rezygnacja. Należy zaznaczyć, że Yield może ustąpić miejsca na rzecz wątku, który zostanie wykonany na tym samym procesorze i posiada priorytet wyższy lub taki sam. Jeśli dany wątek musi poczekać (częsty przypadek w różnych protokołach komunikacji), wtedy może oddać zasoby innemu wątkowi, bo i tak nie ma nic lepszego do roboty w tym czasie.

Thread.Sleep(1) oraz Thread.Sleep(0) mają analogiczne zadanie – tymczasowe ustąpienie miejsca. Różnica polega wyłącznie na tym, jaki wątek może skorzystać z tego przywileju.

Thread.Sleep(0) ustąpi miejsca wątkowi o wyższym lub takim samym priorytecie, wykonanym na dowolnym procesorze.

Thread.Sleep(1) to ustąpienie miejsca wątkowi o dowolnym priorytecie, wykonanym na dowolnym rdzeniu procesora. Metoda najbardziej elastyczna, ale także najwolniejsza. Najszybsza jest metoda Thread.Yield, jednak ma najmniejsze możliwości – ogranicza się jedynie do tego samego rdzenia i wątków o wyższym\takim samym priorytecie.

Struktura SpinWait oraz kolekcja danych bez blokad

Przyszła pora na pokazanie praktycznego przykładu, w którym spinning znacząco optymalizuje synchronizacje.

SpinWait to wyjątkowo sprytna struktura. W przeciwieństwie do poprzednich struktur i klas, SpinWait zmienia zachowanie w zależności od sytuacji.  SpinWait to nie czysty spinning, który na dłuższą metę potrafi spowolnić CPU.

Najważniejsza metoda tej struktury to SpinOnce. Przez pierwsze 10 wywołań, SpinOnce wykonuje po prostu znany już spinning, przez co nie trzeba zmieniać kontekstu. Liczba iteracji zależy od tego, który raz SpinOnce został wywołany. Za pierwszym razem będzie to 2, za drugim 4, potem 8, a kończąc na 2048.

Po 10 wykonaniach, SpinWait orientuje się, że ciągły spinning dławi procesor, i że lepiej uśpić wątek. Kolejne wykonania SpinOnce nie wykonują już spinningu:

  1. Po 10 wywołaniach SpinOnce zaczyna używać metody Thread.Yield, która oddaje wątek z powrotem do CPU i umożliwia zaplanowanie nowego wątku, o wyższym lub takim samym priorytecie, wyłącznie na tym samym procesorze. Yield będzie wywoływany za każdym razem, z wyjątkiem co piątego i dwudziestego wywołania SpinOnce.
  2. Thread.Sleep(0) – podobnie jak Thread.Yield, z tym że wątek może być wykonany na dowolnym rdzeniu (nie tylko na tym samym procesorze). Thread.Sleep(0) będzie wywoływany co piąty raz po 10 wywołaniu SpinOnce.
  3. Thread.Sleep(1) – oddanie wątku na rzecz innego o dowolnym priorytecie, wykonywanym na dowolnym procesorze. Sleep(1) będzie wywoływany co dwudziesty raz po 10 wywołaniu SpinOnce.

Czas pokazać, jak napisać stos bez użycia blokad (źródło MSDN):

public class LockFreeStack<T>
{
    private volatile Node m_head;

    private class Node { public Node Next; public T Value; }

    public void Push(T item)
    {
        var spin = new SpinWait();
        Node node = new Node { Value = item }, head;
        while (true)
        {
            head = m_head;
            node.Next = head;
            if (Interlocked.CompareExchange(ref m_head, node, head) == head) break;
            spin.SpinOnce();
        }
    }
    public bool TryPop(out T result)
    {
        result = default(T);
        var spin = new SpinWait();

        Node head;
        while (true)
        {
            head = m_head;
            if (head == null) return false;
            if (Interlocked.CompareExchange(ref m_head, head.Next, head) == head)
            {
                result = head.Value;
                return true;
            }
            spin.SpinOnce();
        }
    }
}

Push ma za zadanie dołączyć nowy element na początku listy jednokierunkowej.

Należy, zatem ustawić wskaźnik Next na aktualny początek (head) listy a następnie nowy początek listy ustawić na nowy element. Wszystko jest wykonywane w pętli while. Jeśli operację Push wykonuje wyłącznie jeden wątek, całość skończy się po jednej iteracji. W przypadku, gdy wystąpi kolizja i dwa wątki wywołały Push wtedy zostanie wykonanych więcej iteracji.

Interlocked.CompareExchange ma za zadanie ustawić początek listy (m_head) na nowo dodany element node. Interlocked jest oczywiście operacją atomową, więc porównanie m_head z head oraz w przypadku, gdy są one równe, ustawienie m_head na node  zostanie wykonane w jednej, atomowej operacji (bezpiecznej z punktu widzenia współbieżności). W przypadku, gdy drugi wątek zmienił w międzyczasie m_head, wtedy CompareExchange zwróci false  i SpinOnce zostanie wywołany. Po krótkim przeczekaniu, wszystkie operacje zaczynają się od nowa.

W praktyce powyższy algorytm jest szybszy niż lock. W sytuacjach, gdy tylko jeden wątek wywołuje Push wtedy oczywiście widać największą przewagę Push nad implementacji z lock.

Implementacja jest dość mało intuicyjna, ale za to bardzo wydajna. Na szczęście .NET Framework dostarcza bardzo wydajne, współbieżne struktury danych, więc programista zwykle nie musi pisać tego samodzielnie. Warto jednak przeanalizować powyższe rozwiązania, aby móc w przyszłości wykorzystać je już we własnych algorytmach, specyficznych dla danego projektu.

Preferuj SemaphoreSlim zamiast Semaphore, gdy czas oczekiwania na dostęp do sekcji krytycznej jest krótki

Semafor jest dobrze znanym mechanizmem synchronizacji. .NET Framework dostarcza jednak aż dwie klasy: Sempahore oraz SemaphoreSlim. Sposób wykorzystania ich jest bardzo podobny.

Semaphore:

internal class Program
{
   private static int _counter;

   private static void Main(string[] args)
   {
       for (int i = 0; i < 10000; i++)
       {
           var task = new Task(IncreaseValue);
           task.Start();
       }
       Thread.Sleep(2000);
       Console.WriteLine(_counter);

   }
   private static readonly Semaphore _semaphore=new Semaphore(1,1);

   private static void IncreaseValue()
   {
       _semaphore.WaitOne();
       _counter++;
       _semaphore.Release();
   }
}

SemaphoreSlim:

internal class Program
{
    private static int _counter;

    private static void Main(string[] args)
    {
        for (int i = 0; i < 10000; i++)
        {
            var task = new Task(IncreaseValue);
            task.Start();
        }
        Thread.Sleep(2000);
        Console.WriteLine(_counter);
    }
    private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    private static void IncreaseValue()
    {
        _semaphore.Wait();
        _counter++;
        _semaphore.Release();
    }
}

Jaka jest więc różnica? Przede wszystkim należy pamiętać, że SemaphoreSlim to „lekka” wersja semafora. W praktyce wydajniejsza i pochłaniająca mniej zasobów podczas konstrukcji. SemaphoreSlim jest w pełni „managed” – innymi słowy to klasa .NET. Z kolei Semaphore to nic innego jak wrapper na systemowy semafor. Systemowy semafor ma większe możliwości, ponieważ umożliwia synchronizacje wątków, znajdujących się w dwóch różnych procesach (named semaphores).

W .NET 4.5 SemaphoreSlim umożliwia inną ciekawą operację, związaną z wprowadzeniem słów kluczowych async\await. Poniższy przykład nie ma sensu, jeśli używa się async:

private static SemaphoreSlim m_lock = new SemaphoreSlim(1); 

public static async Task DoAsync() 
{ 
    m_lock.Wait(); 
    try 
    { 

    } 
    finally { m_lock.Release(); } 
}

W momencie, gdy wątki będą chciały uzyskać dostęp do sekcji krytycznej, metoda zostanie po prostu zablokowana, co jest mylące, jeśli korzysta się z async – wszyscy spodziewają się, że DoAsync nie będzie blokowała wykonywania kodu. Z tego względu została dodana metoda WaitAsync, która zwraca Task:

private static SemaphoreSlim m_lock = new SemaphoreSlim(1); 

public static async Task DoAsync() 
{ 
    await m_lock.WaitAsync(); 
    try 
    { 

    } 
    finally { m_lock.Release(); } 
}

Powyższa metoda jest w pełni zgodna z asynchronicznym modelem .NET. 4.5. W momencie, gdy uda się uzyskać dostęp do sekcji krytycznej, jej wykonywanie jest wznawiane. Dzięki temu, DoAsync zawsze zachowuje się w sposób asynchroniczny, nawet gdy dwa wątki wywołują ją jednocześnie.

Microsoft radzi, aby SemaphoreSlim preferować dla operacji, które trwają dość krótko (podobnie jak ze spinningiem). W praktyce jest to bardzo często wykorzystywany scenariusz.

Słowo kluczowe volatile

Programiści CPP prawdopodobnie rozpoznają słowo volatile. W środowisku .NET jest ono bardzo mało popularne i niezbyt dobrze udokumentowane. Volatile nakazuje kompilatorowi, aby nie dokonywał ryzykownych optymalizacji, które mogą poskutkować niespodziewanymi efektami ubocznymi. Volatile to zwykły modyfikator pola, tzn.:

class SampleClass
{
    private volatile bool _flag;
}

Dokonywane optymalizacje mają znaczenie w środowisku równoległym i przeważnie w trybie Release. W Debug nie są dokonywane zaawansowane optymalizacje, zatem wspomniane efekty uboczne są raczej niezauważalne.

Dla programistów, którzy pamiętają jeszcze Assembler będzie to dużo łatwiej zrozumieć. Aby nie komplikować, procesor operuje na danych z tzw. rejestrów, a nie bezpośrednio na pamięci operacyjnej czy tym bardziej dysku twardym. Na przykład, aby wykonać operację matematyczną, procesor pobiera najpierw z pamięci liczbę i umieszcza ją w stosownym rejestrze. Wszelkie operacje dokonywane są właśnie na tym rejestrze. Po wykonaniu stosownej logiki, wartość przenoszona jest z rejestru z powrotem do pamięci. W świecie c# widać tylko zmienne, ale w rzeczywistości nawet linia typu i++ powoduje przeniesienie wartości z pamięci do rejestru, wykonanie operacji i z powrotem wynik ląduje w pamięci. Z tego względu, zwykła inkrementacja nie jest operacją atomową i należy do tego użyć klasy Interlocked.

Aby zrozumieć możliwe optymalizacje, warto przeanalizować poniższy fragment kodu:

while( _flag )
{
}

Jeśli w środku pętli flaga _flag nie jest modyfikowana, wtedy optymalizacja, dokonana przez kompilator, mogłaby wyglądać tak:

if(_flag)
{
    while(true)
    {
        // do something
    }
}

Wynik docelowy będzie taki sam, prawda? Dzięki prostemu zabiegowi można uniknąć kopiowania, za każdym razem, zmiennej do rejestru.

Innym problemem są optymalizacje, dokonywane przez sam CPU. Procesor cachuje wartość w rejestrze zamiast za każdym razem kopiować ją z pamięci operacyjnej. Mówiąc ogólnie, dostęp do danych z dysku twardego jest bardzo kosztowny, dostęp do pamięci operacyjnej “średnio”, a z kolei wszelkie operacje na rejestrach są bardzo wydajne. Zrozumiałe jest zatem unikanie operacji na RAM albo zaplanowanie przepływu logiki w taki sposób, aby uzyskać jak najmniej operacji kopiowania między RAM a rejestrami. Z tego względu, procesor ma do dyspozycji cache i kolejne operacje odczytu, które wykonywane są na cachu, a nie na pamięci operacyjnej. Mając w kodzie pętle while odpytującą flagę, kopiowana wartość jest tylko raz, a następne wywołania dotyczą cachu rejestru. Z tego względu, gdy inny wątek zmodyfikuje zmienną, to wartość może nie być widziana przez pozostałe procesory. Jest to trudne do zaobserwowania, ponieważ zależy to od wspomnianego modelu pamięci i architektury procesora. Jednak, istnieje ryzyko, że wartość zmodyfikowana przez inny wątek nie będzie widziana przez wszystkie pozostałe wątki, w czasie gdy korzystają one z wartości buforowanej. Należy zaznaczyć, że wszelka operacja zapisu powoduje aktualizacje całego cache, dostępnego dla wątku\rdzenia\cpu - jest to przeciwne zachowanie do Javy.

Dość teorii, lepiej przyjrzeć się przykładowi. Podany kod może nie zadziałać u wszystkich (zależne od procesora itp.). Należy program skompilować, w trybie Release (aby zostały dokonane optymalizacje), i uruchomić, ale NIE z poziomu Visual Studio! Można np. przejść do katalogu Bin\Release. Podczas swoich testów zaznaczyłem również opcję x64. Poniższy przykład jest modyfikacją kodu z blogu Igora Ostrovsky’ego:

class Program
{
   bool flag=true;
   static void Main(string[] args)
   {
       Program test=new Program();
       var thread = new Thread(() => { test.flag = false; });
       thread.Start();

       while (test.flag)
       {
           // blokada
       }
   }
}

Jaki powinien być właściwy wynik? Pętla while powinna zakończyć działanie po ustawieniu flagi na false przez inny wątek. Takie zachowanie na pewno można zaobserwować w trybie debug. W trybie release odczyt test.flag zostanie jednak zbuforowany, a zapis dokonany przez wątek, nie będzie widoczny dla pętli while. Aby ominąć tę optymalizację i zawsze odczytywać z pamięci, należy użyć słowa volatile:

volatile bool flag=true;
static void Main(string[] args)
{
  Program test=new Program();
  var thread = new Thread(() => { test.flag = false; });
  thread.Start();

  while (test.flag)
  {
      // blokada
  }
}

Kod jest całkowicie bezpieczny – zawsze się zakończy. Jednak wprowadzenie volatile spowoduje stratę, jeśli chodzi o wydajność – za każdym razem w końcu dokonywany jest flush.

Wiadomo, że każdy write powoduje flush. Aby również read powodował flush (odświeżenie cachu), należy oznaczyć pole słowem volatile. Wszelki kod w LOCKU również jest bezpieczny i nie powoduje opisanych wcześniej problemów. Jak już jednak zostało wielokrotnie powiedziane, LOCK jest dużo wolniejszy i lepiej użyć volatile.

Warto również podkreślić, że volatile to nie operator do synchronizacji. Wszelkie operacje, które nie są atomowe nadal są niebezpieczne na polu volatile.

Zakończenie

Artykuł prezentuje wiele sposobów synchronizacji. Wynika z niego, że w programowaniu współbieżnym jest wiele operacji, które można napisać w sposób wydajniejszy. Optymalizacja jednak potrafi być niezwykle trudna i nieumiejętne wybranie mechanizmu może spowodować drastyczny spadek wydajności. Mnogość klas i metod jest dodatkowym utrudnieniem. Jeśli myśli się o programowaniu współbieżnym należy przeanalizować dokładnie wszystkie dostępne klasy oraz sam algorytm. W prostych jednak przypadkach, słowo kluczowe LOCK w zupełności wystarcza – stanowi bezpieczne rozwiązanie, szczególnie dla początkujących programistów lub osób piszących proste algorytmy współbieżne.

 


          

Piotr Zieliński

Absolwent informatyki o specjalizacji inżynieria oprogramowania Uniwersytetu Zielonogórskiego. Posiada szereg certyfikatów z technologii Microsoft (MCP, MCTS, MCPD). W 2011 roku wyróżniony nagrodą MVP w kategorii Visual C#. Aktualnie pracuje w General Electric pisząc oprogramowanie wykorzystywane w monitorowaniu transformatorów . Platformę .NET zna od wersji 1.1 – wcześniej wykorzystywał głównie MFC oraz C++ Builder. Interesuje się wieloma technologiami m.in. ASP.NET MVC, WPF, PRISM, WCF, WCF Data Services, WWF, Azure, Silverlight, WCF RIA Services, XNA, Entity Framework, nHibernate. Oprócz czystych technologii zajmuje się również wzorcami projektowymi, bezpieczeństwem aplikacji webowych i testowaniem oprogramowania od strony programisty. W wolnych chwilach prowadzi blog o .NET i tzw. patterns & practices.