Async\Await–wydajność, część II (implementacja wewnętrzna)

Zanim przejdziemy do pokazania przykładów jak optymalizować async\await najpierw trzeba zrozumieć implementację wewnętrzną w .NET. Bez tego ciężko będzie cokolwiek optymalizować. Na początku async\await wydawał mi się również czymś niezwykłym, a jak zacząłem zagłębiać się w kod IL, okazało się, że to bardzo prosty mechanizm i mógłby być napisany przez każdego z nas.

Kod korzystający z async\await wygląda na synchroniczny. Pod spodem jednak są zwykłe callback’i – dokładnie tak jakby było to napisane w poprzednich wersjach framework’a. Przyjrzyjmy się następującemu przykładowi:

internal class Program
{
   private static void Main(string[] args)
   {
       var task= DoAsync(3);
   }

   private static async Task<int> DoAsync(int inputParameter)
   {
       int localVariable = 3;

       int before = Before();
       int asyncResult1 = await TaskMethod1(inputParameter);
       int afterTaskMethod1 = AfterTaskMethod1();
       int asyncResult2 = await TaskMethod2(inputParameter + asyncResult1+localVariable);
       AfterTaskMethod2();

       return asyncResult1 + asyncResult2 + inputParameter;
   }

   private static int Before()
   {
       return 1;
   }

   private static int AfterTaskMethod1()
   {
       return 1;
   }

   private static int AfterTaskMethod2()
   {
       return 1;
   }

   private static Task<int> TaskMethod1(int arg)
   {
       return Task.Factory.StartNew(() => arg);
   }

   private static Task<int> TaskMethod2(int arg)
   {
       return Task.Factory.StartNew(() =>
       {
           Thread.Sleep(500000);
           return arg;
           ;
       });
   }
}

Po kolei… Mamy asynchroniczną metodę DoAsync. Najpierw wywołuje ona zwykłą synchroniczną metodę Before. Kolejne wywołanie to TaskMethod1, która jest już asynchroniczna i korzystamy tutaj z await, aby nie blokować wywołania. Potem wywołujemy znów synchroniczną metodę a na końcu TaskMethod2, która jest asynchroniczna. Przekazujemy także wynik z TaskMethod1 do niej. Na końcu zwracamy jakąś tam liczbę, która składa się z z wyników dostarczonych przez poszczególne metody.

Logika nie ma kompletnie sensu tutaj. Stworzyłem tak przykład, aby pokazać jak to działa, gdy przeplatamy ze sobą asynchroniczne i synchroniczne metody.

Proszę zauważyć, że DoAsync zwraca nie integer ale Task<int>. Wszystkie metody oznaczone słowem async muszą zwracać Task (jawnie lub niejawnie). Teraz możemy zajrzeć do Reflector’a, aby przekonać się, co tak naprawdę jest generowane. W DoAsync mamy:

[AsyncStateMachine(typeof(<DoAsync>d__0)), DebuggerStepThrough]
private static unsafe Task<int> DoAsync(int inputParameter)
{
    <DoAsync>d__0 d__;
    Task<int> task;
    AsyncTaskMethodBuilder<int> builder;
    &d__.inputParameter = inputParameter;
    &d__.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
    &d__.<>1__state = -1;
    builder = &d__.<>t__builder;
    &builder.Start<<DoAsync>d__0>(&d__);
    task = &&d__.<>t__builder.Task;
Label_003C:
    return task;
}

Proszę zauważyć, że nie ma tutaj słowa kluczowego async – tak naprawdę CLR nic o tym nie wiem. Cała funkcjonalność async\await nie wprowadza modyfikacji w CLR. Podczas kompilacji jest po prostu generowany specjalny kod, przedstawiony wyżej.

Przede wszystkim wygenerowana jest maszyna stanów. Atrybut [AsyncStateMachine(typeof(<DoAsync>d__0)), DebuggerStepThrough] po prostu wskazuje typ implementacji tej maszyny.

Maszyna stanów zawiera w formie pól publicznych wszelkie zmienne będące w obrębie metody async (w naszym przypadku DoAsync). Na przykład w DoAsync mamy inputParameter i jak widać w maszynie stanów również mamy publiczne pole inputParameter. Z kolei localVariable zostało przeniesione do samej implementacji maszyny, ponieważ nie ma konieczności ustawiania jej z poziomu DoAsync. Pole state określa aktualny stan maszyny – o tym później. Proszę zauważyć, że StateMachine zwraca Task, czyli wątek reprezentujący operację.

Przejdźmy teraz do samej implementacji StateMachine:

[CompilerGenerated]
private struct <DoAsync>d__0 : IAsyncStateMachine
{
    // Fields
    public int <>1__state;
    public AsyncTaskMethodBuilder<int> <>t__builder;
    private object <>t__stack;
    private TaskAwaiter<int> <>u__$awaiter6;
    public int <afterTaskMethod1>5__4;
    public int <asyncResult1>5__3;
    public int <asyncResult2>5__5;
    public int <before>5__2;
    public int <localVariable>5__1;
    public int inputParameter;

    // Methods
    private unsafe void MoveNext()
    {
        bool flag;
        int num;
        Exception exception;
        int num2;
        TaskAwaiter<int> awaiter;
        TaskAwaiter<int> awaiter2;
        int num3;
    Label_0000:
        try
        {
            flag = 1;
            num2 = this.<>1__state;
            switch ((num2 - -3))
            {
                case 0:
                    goto Label_0028;

                case 1:
                    goto Label_0034;

                case 2:
                    goto Label_0034;

                case 3:
                    goto Label_002D;

                case 4:
                    goto Label_002F;
            }
            goto Label_0034;
        Label_0028:
            goto Label_016F;
        Label_002D:
            goto Label_0089;
        Label_002F:
            goto Label_011A;
        Label_0034:;
        Label_0036:
            this.<localVariable>5__1 = 3;
            this.<before>5__2 = Program.Before();
            awaiter = Program.TaskMethod1(this.inputParameter).GetAwaiter();
            if (&awaiter.IsCompleted != null)
            {
                goto Label_00A8;
            }
            this.<>1__state = 0;
            this.<>u__$awaiter6 = awaiter;
            &this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<DoAsync>d__0>(&awaiter, this);
            flag = 0;
            goto Label_019F;
        Label_0089:
            awaiter = this.<>u__$awaiter6;
            this.<>u__$awaiter6 = new TaskAwaiter<int>();
            this.<>1__state = -1;
        Label_00A8:
            int introduced7 = &awaiter.GetResult();
            awaiter = new TaskAwaiter<int>();
            num3 = introduced7;
            this.<asyncResult1>5__3 = num3;
            this.<afterTaskMethod1>5__4 = Program.AfterTaskMethod1();
            awaiter = Program.TaskMethod2((this.inputParameter + this.<asyncResult1>5__3) + this.<localVariable>5__1).GetAwaiter();
            if (&awaiter.IsCompleted != null)
            {
                goto Label_0139;
            }
            this.<>1__state = 1;
            this.<>u__$awaiter6 = awaiter;
            &this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<DoAsync>d__0>(&awaiter, this);
            flag = 0;
            goto Label_019F;
        Label_011A:
            awaiter = this.<>u__$awaiter6;
            this.<>u__$awaiter6 = new TaskAwaiter<int>();
            this.<>1__state = -1;
        Label_0139:
            int introduced8 = &awaiter.GetResult();
            awaiter = new TaskAwaiter<int>();
            num3 = introduced8;
            this.<asyncResult2>5__5 = num3;
            Program.AfterTaskMethod2();
            num = (this.<asyncResult1>5__3 + this.<asyncResult2>5__5) + this.inputParameter;
            goto Label_0189;
        Label_016F:
            goto Label_0189;
        }
        catch (Exception exception1)
        {
        Label_0171:
            exception = exception1;
            this.<>1__state = -2;
            &this.<>t__builder.SetException(exception);
            goto Label_019F;
        }
    Label_0189:
        this.<>1__state = -2;
        &this.<>t__builder.SetResult(num);
    Label_019F:
        return;
    }

    [DebuggerHidden]
    private unsafe void SetStateMachine(IAsyncStateMachine param0)
    {
        &this.<>t__builder.SetStateMachine(param0);
        return;
    }
}
 

Najważniejsza metoda to MoveNext. Służy ona do wykonania kolejnego stanu. Co jest zatem poszczególnym stanem? Przyjrzyjmy się pierwszemu z nich:

this.<localVariable>5__1 = 3;
this.<before>5__2 = Program.Before();
awaiter = Program.TaskMethod1(this.inputParameter).GetAwaiter();
if (&awaiter.IsCompleted != null)
{
 goto Label_00A8;
}
this.<>1__state = 0;
this.<>u__$awaiter6 = awaiter;
&this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<DoAsync>d__0>(&awaiter, this);
flag = 0;

Najpierw wywoływana jest synchroniczna metoda Before a wynik jest przechowywany znów w polu klasy. Następnie przechodzimy do TaskMethod1. Ze względu, że jest ona asynchroniczna, korzystamy ze specjalnej klasy, TaskWaiter’a. Więcej tak naprawdę nie możemy zrobić w tym stanie więc wywołujemy return. Teraz musimy poczekać na zakończenie metody TaskMethod1. W momencie jej zakończenia, MoveNext znów zostanie wywołany, ale teraz pole state będzie zawierało numer kolejnego stanu, co spowoduje, że następujący kod zostanie wywołany:

int introduced7 = &awaiter.GetResult();
awaiter = new TaskAwaiter<int>();
num3 = introduced7;
this.<asyncResult1>5__3 = num3;
this.<afterTaskMethod1>5__4 = Program.AfterTaskMethod1();
awaiter = Program.TaskMethod2((this.inputParameter + this.<asyncResult1>5__3) + this.<localVariable>5__1).GetAwaiter();
if (&awaiter.IsCompleted != null)
{
 goto Label_0139;
}
this.<>1__state = 1;
this.<>u__$awaiter6 = awaiter;
&this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<int>, Program.<DoAsync>d__0>(&awaiter, this);
flag = 0;
goto Label_019F;

Analogicznie w kolejnym stanie najpierw wywołujemy synchroniczną metodę AfterTaskMethod1, a potem, za pomocą TaskAwaiter’a wywołujemy TaskMethod2. Synchroniczne metody nie potrzebują żadnej szczególnej opieki, dlatego nie są one kryterium rozdzielania metody na kilka stanów.

Na końcu, ostatni stan wykonuje:

int introduced8 = &awaiter.GetResult();
awaiter = new TaskAwaiter<int>();
num3 = introduced8;
this.<asyncResult2>5__5 = num3;
Program.AfterTaskMethod2();
num = (this.<asyncResult1>5__3 + this.<asyncResult2>5__5) + this.inputParameter;

&this.<>t__builder.SetResult(num);

Kod po prostu zwróci wyjątek z rezultatem ustawionym na to, co w oryginalnym kodzie mamy w return.

Wiem, że w moim opisie pominąłem wiele stanów ale nie ma to znaczenia. Ogólnie zasada polega na wygenerowaniu nowej klasy, opakowującej lokalne zmienne i parametry wejściowe. Jak widać, nie ma żadnej czarnej magii tutaj – wszystko to stary, dobrze znany kod c#. Cała maszyna jest wykonywana w osobnym wątku i jak widać w DoAsync, natychmiast jest zwracany wątek:

[AsyncStateMachine(typeof(<DoAsync>d__0)), DebuggerStepThrough]
private static unsafe Task<int> DoAsync(int inputParameter)
{
    <DoAsync>d__0 d__;
    Task<int> task;
    // inicjalizacja maszyny itp.
    task = &&d__.<>t__builder.Task;
Label_003C:
    return task;
}

DoAsync zatem nie blokuje wywołania. Reszta logiki jest zawarta w MoveNext – wewnętrznie pole state jest aktualizowane co przełącza logikę do konkretnego stanu. Kryterium podziału metody asynchronicznej na stany jest liczba await.

W następnym postach będę kontynuował tematykę async\await i pokażę, jak drobne zmiany mają wpływ na to, co zostanie wygenerowane w StateMachine.

Async\Await–wydajność, część I

W kolejnych kilku wpisach, zajmiemy się async\await ale od strony wydajnościowej. Jeśli ktoś nie wie do czego te słowa kluczowe służą, zachęcam przeczytać np. mój artykuł o programowaniu asynchronicznym w .NET 4.5.

Sposób korzystania z async\await jest bardzo prosty. Do tego stopnia, że programowanie asynchroniczne wygląda w zasadzie tak jak synchroniczne podejście. W kodzie nie ma callback’ow a przepływ logiki wygląda na sekwencyjny. Niestety, trzeba być świadomym jakie pułapki czekają na programistów.

Jedną z nich jest przekazanie kontekstu. Wiadomo, żeby zaktualizować interfejs należy skorzystać z SynchronizationContenxt, który potrafi wpompować dane do UI Thread. Await pobiera kontekst w sposób niejawny, przed wywołaniem asynchronicznej metody, a potem po jej zakończeniu, wykorzystywany jest on aby powrócić do wykonywania kodu.

Pobranie kontekstu ma swoją cenę i może spowodować problemy. Poniższy kod jest dowodem, że kontekst jest naprawdę przekazywany:

private static async Task DoAsync()
{
  await AnotherMethodAsync();
  // any logic
}

private static void AnotherMethod()
{
  Thread.Sleep(1000);
 // any logic
}
private static Task AnotherMethodAsync()
{
  Task task = Task.Factory.StartNew(AnotherMethod);
  return task;
}

private void Button_Click_1(object sender, RoutedEventArgs e)
{
  Task task = DoAsync();
  task.Wait();
  MessageBox.Show("End");
}

Spodziewalibyśmy się, że na ekranie pojawi się wiadomość. Niestety mamy do czynienia z deadlock. W DoAsync korzystamy z await, który przed wejściem do metody, pobierze kontekst, aby po jej zakończeniu móc kontynuować. W tym problem, że my wywołujemy Task.Wait, który będzie blokował wywołanie aż do momentu zakończenia AnotherMetodAsync. Ta metoda jednak nigdy się nie zakończy bo await czeka aż będzie mógł wpompować dane.

Innymi słowy, await czeka na zakończenie task.Wait co nigdy się nie stanie bo task.Wait czeka aż await się zakończy. Z tego względu dobrą praktyką jest unikanie Wait i korzystanie z await wszędzie:

private async void Button_Click_1(object sender, RoutedEventArgs e)
{
  await DoAsync();
  MessageBox.Show("End");
}

Jak wspomniałem, przekazanie kontekstów ma swoją cenę. Zwykle to jest to czego oczekujemy ponieważ po wykonaniu asynchronicznego kodu, chcemy np. zaktualizować interfejs. Czasami jednak, mamy w pętli serie await i nie ma sensu za każdym razem pobierać kontekstu. Wtedy można skorzystać z metody ConfigureAwait i przekazać false, co oznacza, że kontekst nie zostanie pobrany:

private static async Task DoAsync()
{
  await AnotherMethodAsync().ConfigureAwait(false);
  // any logic
}

private static void AnotherMethod()
{
  Thread.Sleep(1000);
 // any logic
}
private static Task AnotherMethodAsync()
{
  Task task = Task.Factory.StartNew(AnotherMethod);
  return task;
}

private void Button_Click_1(object sender, RoutedEventArgs e)
{
  Task task = DoAsync();
  task.Wait();
  MessageBox.Show("End");
}

Powyższy kod nie spowoduje zakleszczenia ponieważ nie pobieramy kontekstu oraz nie próbujemy do niego wrócić po zakończeniu await. Należy uważać kiedy z takiego podejścia korzystamy. W przypadku, gdy piszemy bibliotekę, wykonującą w jakieś pętli różne metody asynchroniczne wtedy pobieranie kontekstu za każdy razem jest zmarnowaniem zasobów. Powyższe rozważania nie tyczą się wyłącznie SynchronizationContext ale wszystkich dostępnych w .NET.

TPL Dataflows – część IX (TransformManyBlock)

Dzisiaj na szybko wrócimy z powrotem do tematu TPL Data Flows. Jakiś czas temu pisałem o różnych blokach w TPL DataFlows. Nie opisałem jednak TransformManyBlock.  TransformManyBlock jest analogiczny do TransformBlock z tym, że na wyjściu jest kolekcja więc możliwe jest zwrócenie kilku wartości. TransformBlock służył wyłącznie do przetworzenia parametru i zwrócenia pojedynczego wyniku.

Ktoś może zapytać, czy to nie to samo co przekazanie jako typu generycznego IEnumerable<> do TransformBlock – też wtedy na wyjściu będzie kolekcja danych. Najlepiej to pokazać na przykładzie:

internal class Program
{
   private static void Main()
   {
       var block = new TransformManyBlock<int, double>(number => TransformData(number));
       block.LinkTo(new ActionBlock<double>(n => Display(n)));

       block.Post(3);
     
       Console.ReadLine();
   }

   private static void Display(double n)
   {
       Console.WriteLine("Liczba:{0}",n);
   }

   private static IEnumerable<double> TransformData(int number)
   {
       for (int i = 0; i < number; i++)
           yield return i;
   }
}

Jak widać, akcja podłączona do TransformManyBlock wciąż przyjmuje pojedynczą wartość jako  parametr wejściowy. Po prostu wartości będą przekazane jedna po jednej. Zmodyfikujmy przykład, a konkretnie metodę TransformData:

private static IEnumerable<double> TransformData(int number)
{
  for (int i = 0; i < number; i++)
  {
      yield return i;
      Thread.Sleep(5000);
  }
}

Dodaliśmy opóźnienie. W momencie jednak wygenerowania pierwszej wartości, zostanie ona natychmiast przekazana do ActionBlock. Możliwe zatem, że dane częściowe z dwóch różnych źródeł danych będą  przeplatać się wzajemnie. Nie będzie trzeba czekać aż wszystkie elementy kolekcji są gotowe do przekazania.  Myślę, że widać sporą analogię do SelectMany. TransformBlock można porównać do Select, z kolei TransformManyBlock do SelectMany. Oba bloki to tak naprawdę projekcja danych.

Wydajność: jak to jest z wyjątkami?

W sprawie wydajności wyjątków można znaleźć wiele opinii, często sprzecznych ze sobą. W dzisiejszym wpisie przedstawię kilka programików, mających na celu, wyjaśnienie jaki wpływ mają wyjątki oraz ich łapanie na wydajność aplikacji. Zacznijmy od przykładów a potem przejdziemy do analizy wyników. Kod z wyrzucaniem wyjątków:

internal class Program
{        
   private static void Main(string[] args)
   {
       const int n = 20000;

       Stopwatch stopwatch = Stopwatch.StartNew();

       for (int i = 0; i < n; i++)
       {
           try
           {
               GetValueWithException();
           }
           catch 
           {                    
           }
       }

       Console.WriteLine(stopwatch.ElapsedTicks);
   }
   private static object GetValueWithException()
   {
       throw new Exception();
   }
}

Wynik w trybie Debug to 900167.

Następnie analogiczna konstrukcja zwracająca po prostu NULL:

internal class Program
{        
   private static void Main(string[] args)
   {
       const int n = 20000;

       Stopwatch stopwatch = Stopwatch.StartNew();

       for (int i = 0; i < n; i++)
       {
        if (GetValueWithoutException() == null)
        {
            
        }
       }

       Console.WriteLine(stopwatch.ElapsedTicks);
   }
   private static object GetValueWithoutException()
   {
       return null;
   }
}

W trybie Debug uzyskano 760. Różnica jest więc kolosalna. Dlaczego wyrzucanie wyjątków jest tak wolne?

Najpierw musimy stworzyć nowy obiekt Exception, który jest klasą. Wiąże się  to z zadeklarowaniem pamięci na stercie. Następnie GC będzie musiał te wszystkie obiekty zebrać co jest ogromnym wyzwaniem i wpływa zdecydowanie niekorzystnie na wydajność.  W drugim rozwiązaniu zwracamy po prostu NULL czyli status operacji.

Należy zdać sobie sprawę, że wyjątki to coś więcej niż status wykonania operacji. Zaglądając do klasy Exception, znajdziemy tam wiele informacji o kontekście operacji, która nie powiodła się. Mamy do dyspozycji stacktrace, który zawiera informacje o tym, jakie metody były wykonane. Co za tym idzie, im większy stacktrace tym prawdopodobnie wyrzucenie wyjątku będzie trwało dłużej (więcej informacji do zebrania). Ponadto, wyjątek po drodze może zostać złapany i ponownie wyrzucony co jeszcze bardziej zwiększa czas wykonania. Tekst wyjątku, warto zauważyć jest zawsze w lokalnym języku – lokalizacja jest również drobnym obciążeniem. Istnieje możliwość zagnieżdżonych try-catch. Innymi słowy, wydajność wyjątków w dużej mierze zależy od głębokości stosu.

Inną ciekawą przyczyną dlaczego wyjątki są wolniejsze jest pamięć podręczna CPU. Wyrzucając wyjątek, zostanie ona zmieniona a co za tym idzie, w dalszej części algorytmu\programu będziemy mieli do czynienia z “cache misses’, co oznacza, że dane na których wcześniej operowaliśmy znów będą musiały zostać załadowane do cache.

Wyjątki jeszcze wolniejsze są w sytuacji, gdy wyrzucamy je pomiędzy AppDomain lub kodem zarządzanym\niezarządzanym – wtedy muszą być serializowane co dodatkowo obciąża CPU.

Wyjątki są traktowane w sposób specjalny przez CLR lub nawet Visual Studio. Każdy wyrzucony wyjątek jest od razu wyświetlany w oknie output. Oczywiście mowa tutaj o trybie Debug. Ponadto, mamy do dyspozycji wiele handlerów zarówno w AppDomain jak i w klasach typu Application (w zależności czy korzystamy z WPF czy WinForms).

Czy to znaczy, że mamy unikać wyjątków? NIE! Powyższy przykład jest tylko dowodem na to, że wyjątki nie służą do sterowania przepływem logiki. Oczywiście jest to zła praktyka i post udowadnia to z punktu wydajnościowego. Wyjątki powinny być używane gdy coś “wyjątkowego” zdarzyło się. Jeśli wyrzucamy kilka tysięcy wyjątków w ciągu godziny wtedy ma to niekorzystny wpływ na wydajność ale to znaczy , że architektura jest zła i wyjątki zostały użyte po prostu w niewłaściwy sposób.

Na koniec inny przykład:

internal class Program
{        
   private static void Main(string[] args)
   {
       const int n = 20000;

       Stopwatch stopwatch = Stopwatch.StartNew();

       for (int i = 0; i < n; i++)
       {
           try
           {
               GetValueWithException();
           }
           catch (Exception)
           {                    
           }
       }

       Console.WriteLine(stopwatch.ElapsedTicks);
   } 
   private static object GetValueWithException()
   {
       return null;
   }
}

Wynik to 700. Jeśli nie wyrzucamy wyjątków wtedy catch nie ma praktycznie żadnego obciążenia. “Problemy” zaczynają się wyłącznie wtedy, gdy korzystamy z throw a nie catch.

NUnit w Visual Studio 2012 (pokrycie kodu)

Visual Studio 2012 ma wsparcie dla nUnit. Wcześniej wspierał oficjalnie wyłącznie własne testy jednostkowe. Należy jednak najpierw zainstalować adapter. Wystarczy przejść do Tools –> Extensions and Updates –> Online –> Visual Studio Gallery –> Tools –> Testing i wybrać NUnit Test adapter:

image

Po instalacji do dyspozycji mamy Test Explorer (VS):

image

Najciekawszą chyba częścią jest możliwość sprawdzenia pokrycia kodu przez testy:

image

Klikając na którąś z pozycji zostaniemy przekierowani bezpośrednio do kodu. Odpowiednim kolorem zostanie zaznaczone, który kod został pokryty przez testy:

image

Moim zdaniem jest to świetne rozwiązanie. W kilka sekund można przekonać się, czy wszystkie ważne części kodu zostały przetestowane. Nie trzeba instalować jakiś zewnętrznych narzędzi, wszystko jest ładnie zintegrowane z Visual Studio.

Code review: struktura danych

Często spotykam następujące konstrukcje:

class RiskInfo
{
    public int Condition{get;set;}
    public double Score{get;set;}
}

Innymi słowy, kontener na kilka prostych zmiennych. Jeśli ktoś nie zna zasady działania Garbage Collector, gorąco zachęcam do przeczytania np. mojego cyklu artykułów o GC. Usunięcie obiektu z pamięci to nie prosta sprawa i naprawdę wiele musi zostać wykonanych operacji w tle. Z tego względu, jeśli klasa jest mała i posiada wiele instancji, wtedy dużo lepiej skonwertować ją do struktury. Dużo łatwiej usunąć 100 instancji struktur ze stosu niż 100 instancji klas z zarządzanej sterty. Z mojego doświadczenia wynika, że programiści kojarzą struktury ale jakoś w praktyce z nich nie korzystają. Warto zastanowić się czy w projekcie nie mamy obiektów, spełniających powyższe kryteria.

Niestety bardzo często struktury są źle wykorzystywane.  Ktoś mógłby napisać następujący kod:

struct RiskInfo
{
    public int Condition{get;set;}
    public double Score{get;set;}
}

Niestety powyższy kod jest przykładem złej praktyki. Każda struktura musi być zawsze immutable (niezmienna). O ogólnych korzyściach wynikających z niezmienności obiektów pisałem już tutaj. W kontekście struktur jest to naprawdę ważne. Wyobraźmy sobie następujący kod:

internal class Program
{
   private static void Main(string[] args)
   {
       RiskInfo[] risks=new RiskInfo[2];
       RiskInfo riskInfo = risks[0];
       riskInfo.Condition = 10;

       Console.WriteLine(risks[0].Condition);
   }  
}

Nie jest wymagane od programisty, aby wiedział jak wewnętrznie został zaimplementowany RiskInfo. Stąd powyższy kod ma jak najbardziej sens. Problem w tym, że na ekranie wyświetli się 0 a nie 10. Bez wiedzy o tym, że RiskInfo to struktura łatwo popełnić błąd. Podobnie z właściwościami:

internal class Program
{
   private static void Main(string[] args)
   {
       RiskInfo.Condition = 10;
   }
   public static RiskInfo RiskInfo { get; set; }
}

Na szczęście powyższy problem zostanie już wykryty na etapie kompilacji. Szczególnie niebezpieczny kod (znaleziony na blogu Eric Lippert’a) to:

internal class Program
{
   public static readonly RiskInfo RiskInfo=new RiskInfo();

   private static void Main(string[] args)
   {
       Console.WriteLine(RiskInfo.CalculateScore());
       Console.WriteLine(RiskInfo.CalculateScore());
       Console.WriteLine(RiskInfo.CalculateScore());
   }
}
struct RiskInfo
{
   public int Condition { get; set; }
   public double Score { get; set; }

   public double CalculateScore()
   {
       Score = Score + 1;
       return Score;
   }
}

Co na ekranie wyświetli się? Każdy spodziewa chyba się 1,2,3 ale jednak będzie to 1,1,1. Dlaczego? Spróbujmy to wyjaśnić.  Jeśli usunęlibyśmy readonly wtedy problem by nie istniał. Na ekranie byśmy zobaczyli oczekiwane 1,2,3. Aby to wyjaśnić musimy zajrzeć do specyfikacji c# tutaj. Znajdziemy tam następujący cytat:

“If E is a value, or if the field is readonly and the reference occurs outside an instance constructor of the struct in which the field is declared, then the result is a value, namely the value of the field I in the struct instance given by E. “

Innymi słowy, jeśli odwołujemy się do zmiennej spoza konstruktora, wtedy nie operujemy bezpośrednio na niej a na wartości. Czyli to tak samo, jakbyśmy mieli właściwość zwracającą RiskInfo – wykonana zostanie kopia za każdym razem. W powyższym przykładzie, operujemy na 3 różnych kopiach, ponieważ po za konstruktorem, readonly zachowuje się bardziej jak właściwość – nie operuje na zmiennej.  Przenieśmy logikę do konstruktora i zobaczymy na ekranie 1,2,3:

internal class Program
{
   public readonly RiskInfo RiskInfo=new RiskInfo();
   
   public Program()
   {
       Console.WriteLine(RiskInfo.CalculateScore());
       Console.WriteLine(RiskInfo.CalculateScore());
       Console.WriteLine(RiskInfo.CalculateScore());
   }

   private static void Main(string[] args)
   {            
       Program program=new Program();
   }
}

Takich kruczków jest naprawdę wiele. Z tego względu dużo lepiej zaimplementować RiskInfo w następujący sposób:

internal struct RiskInfo
{
   public readonly int Condition;
   public readonly double Score;

   public RiskInfo(int condition, double score) : this()
   {
       Condition = condition;
       Score = score;
   }
}

Code Review: Publiczny enum

Załóżmy, że w kodzie zobaczymy enum’a np.:

public enum Colors
{
   Black,
   Red,
   Yellow
};

Jeśli budujemy framework lub jakąkolwiek bibliotekę należy być świadomym zagrożeń wynikających z wewnętrznej budowy enum’a. Warto zajrzeć do Reflector’a aby przekonać się o tym:

.class public auto ansi sealed Colors
    extends [mscorlib]System.Enum
{
    .field public static literal valuetype ConsoleApplication1.Colors Black = int32(0)

    .field public static literal valuetype ConsoleApplication1.Colors Red = int32(1)

    .field public specialname rtspecialname int32 value__

    .field public static literal valuetype ConsoleApplication1.Colors Yellow = int32(2)

}

Problemem są oczywiście stałe. Powyższy enum wygląda mniej więcej następująco:

public static class CustomColors
{
   public const int Black = 0;
   public const int Red = 1;
   public const int Yellow = 2;
}

Jeśli ktoś nie kojarzy problemów ze stałymi to zapraszam do mojego wpisu tutaj. W skrócie, stała jest wstrzykiwana na etapie kompilacji. Po kompilacji, miejsce gdzie odwołujemy się do stałej jest zastępowane konkretną wartością. Jeśli piszemy system modularny wtedy w końcu możliwe jest, że biblioteka ze stałymi zostanie zmodyfikowana. Niestety wszystkie zależne biblioteki nie będą odzwierciedlać tej zmiany bez ponownej kompilacji. Oczywiście jeśli stała jest faktycznie niezmienna (klasyczny przykład PI) wtedy nie ma z tym problemu. Dobrą praktyką jednak jest (jeśli nie ma się do tego pewności) użycie pól read-only zamiast const, jeśli musimy zadeklarować dane pole jako publiczne. Każdy element publiczny powinien być zaprojektowany w ten sposób, że nie wymaga rekompilacji – w końcu udostępniamy go innym programistom.

Jeśli powyższe ryzyka faktycznie są nie do zaakceptowania dla danego projektu, wtedy po prostu lepiej napisać:

public static class CustomColors
{
   public static readonly int Black = 0;
   public static readonly int Red = 1;
   public static readonly int Yellow = 2;
}

Zostanie to przetłumaczone na:

.class public abstract auto ansi sealed beforefieldinit CustomColors
    extends [mscorlib]System.Object
{
    .method private hidebysig specialname rtspecialname static void .cctor() cil managed
    {
    }


    .field public static initonly int32 Black

    .field public static initonly int32 Red

    .field public static initonly int32 Yellow

}

 

Liczba dni pomiędzy dwoma datami

Może komuś się przyda. Mając do dyspozycji dwie zmienne DateTime można skorzystać z niejawnej konwersji do TimeSpan i użyć właściwości TotalDays. TotalDays jest typu double więc możliwe wartości to również np. 4.5 dnia.:

DateTime timestamp1=new DateTime(2010,1,20);
DateTime timestamp2 = new DateTime(2010, 1, 25);
TimeSpan timeSpan = timestamp2 - timestamp1;
double daysNumber = timeSpan.TotalDays;

Console.WriteLine(daysNumber);

Postsharp – aspekty metod (OnMethodBoundaryAspect)

W ostatnim wpisie przedstawiłem aspekt do obsługi wyjątków. Dzisiaj trochę więcej o samych aspektach dla metod. Oprócz wyświetlenia  nazwy metody czy przekazanych parametrów można wyciągnąć trochę więcej informacji. Dla przypomnienia, parametry można wyświetlać następująco:

public override void OnEntry(MethodExecutionArgs args) 
{ 
    var argValues = new StringBuilder(); 
    
    foreach (var argument in args.Arguments) 
    { 
        argValues.Append(argument.ToString()).Append(","); 
    } 
 
    Console.WriteLine("The {0} method was entered with the parameter values: {1}", 
    args.Method.Name, argValues.ToString()); 
} 

Możliwe jest również czytanie lub modyfikowanie wartości zwracanej. Przykład:

public class MyAspect : OnMethodBoundaryAspect
{
   public override void OnExit(MethodExecutionArgs args)
   {
       base.OnExit(args);
       Console.WriteLine("Orginalna wartosc:{0}",args.ReturnValue);
       args.ReturnValue = "Test";
   } 
}    

internal class Program
{
   private static void Main()
   {
       Console.WriteLine(GetText());
   }          
   [MyAspect]
   private static string GetText()
   {
       return "Witaj swiecie.";
   }
}

Właściwość ReturnValue jest do odczytu i zapisu więc można zmienić to co funkcja początkowo zwróciła.

Można również mieć dostęp do klasy, do której aspekt został doczepiony tzn.:

public class MyAspect : OnMethodBoundaryAspect
{
   public override void OnEntry(MethodExecutionArgs args)
   {
       base.OnEntry(args);
       Console.WriteLine(args.Instance.ToString());
   }
}

internal class Wrapper
{
   [MyAspect]
   public string GetText()
   {
       return "Witaj swiecie.";
   }
}

internal class Program
{
   private static void Main()
   {
       Wrapper wrapper=new Wrapper();
       Console.WriteLine(wrapper.GetText());
   }                
}

W powyższym przykładzie, Instance zawiera referencję na obiekt klasy Wrapper.

Z kolei właściwość Method zawiera informację o metodzie w formie MethodBase:

public override void OnEntry(MethodExecutionArgs args)
{
    base.OnEntry(args);
    Console.WriteLine(args.Method.Name);
    Console.WriteLine(args.Method.IsVirtual);
}

Ciekawą właściwością jest FlowBehaviour. FlowBehaviour przyjmuje następujące wartości: Default, Continue, RethrowException, Return, ThrowException. Poniższy kod wyświetli tylko jedną wiadomość ponieważ w aspekcie ustawiamy FlowBehaviour na Return co spowoduje po prostu wykonanie return przed logiką metody:

[Serializable]
public class MyAspect : OnMethodBoundaryAspect
{
   public override void OnEntry(MethodExecutionArgs args)
   {
       if ((bool)args.Arguments[0])
           args.FlowBehavior = FlowBehavior.Return;
   }
}
internal class Program
{
   private static void Main()
   {
       DoSomething(true);
       DoSomething(false);
   }
   [MyAspect]
   public static void DoSomething(bool flag)
   {
       Console.WriteLine("Witaj swiecie:{0}",flag);
   }
}

RethrowException z kolei jest przydatny, gdy przeciążamy OnException:

public class MyAspect : OnMethodBoundaryAspect
{
   public override void OnException(MethodExecutionArgs args)
   {
       Console.WriteLine("Zlapano wyjatek");
       args.FlowBehavior = FlowBehavior.RethrowException;
   }
}
internal class Program
{
   private static void Main()
   {
       DoSomething(true);
       DoSomething(false);
   }
   [MyAspect]
   public static void DoSomething(bool flag)
   {
       throw new NotImplementedException();
   }
}

RethrowException jak sama nazwa mówi, wyrzuci ponownie ten sam wyjątek. Najpierw on zostanie złamany w celu wywołania OnException na aspekcie a potem przez RethrowException zostanie on ponownie wyrzucony.

Kolejną wartością jest ThrowException, który wyrzuci nowy wyjątek zdefiniowany w aspekcie:

[Serializable]
public class MyAspect : OnMethodBoundaryAspect
{
   public override void OnException(MethodExecutionArgs args)
   {
       Console.WriteLine("Zlapano wyjatek");
       args.Exception=new Exception("Custom exception");
       args.FlowBehavior = FlowBehavior.ThrowException;
   }
}
internal class Program
{
   private static void Main()
   {
       DoSomething(true);
       DoSomething(false);
   }
   [MyAspect]
   public static void DoSomething(bool flag)
   {
       throw new NotImplementedException();
   }
}

Jeśli nie ustawilibyśmy właściwości Exception wtedy FlowBehaviour.ThrowException i FlowBehaviour.RethrowException miałby takie same znaczenie.

Z kolei Continue zdławi wyjątek i należy tego w większości sytuacjach unikać jeśli chodzi o metodę OnException:

[Serializable]
public class MyAspect : OnMethodBoundaryAspect
{
   public override void OnException(MethodExecutionArgs args)
   {
       Console.WriteLine("Zlapano wyjatek");
       args.FlowBehavior = FlowBehavior.Continue;
   }
}

Domyślną wartością jest FlowBehaviour.Default co oznacza, że dla OnException będzie to RethrowException a dla OnExit\OnEntry Continue.

Ostatnią właściwością jaką chciałem omówić jest MethodExceuctionTag. Jeśli zachodzi potrzeba dzielenia stanu pomiędzy poradami (OnEntry, OnExit)  możemy właśnie z tego skorzystać. Na przykład aspekt sprawdzający jak długo metoda była wykonywana można zaimplementować następująco:

[Serializable]
public class PerformanceAspect : OnMethodBoundaryAspect
{
   public override void OnEntry(MethodExecutionArgs args)
   {
       args.MethodExecutionTag = Stopwatch.StartNew();
   }

   public override void OnExit(MethodExecutionArgs args)
   {
       var sw = (Stopwatch)args.MethodExecutionTag;
       sw.Stop();

       Console.WriteLine("{0}:{1}", args.Method.Name,sw.ElapsedMilliseconds);
   }
} 

Wyłącznie w powyższy sposób należy przekazywać dane pomiędzy zdarzeniami – nie można polegać na prywatnych polach jak to byśmy zrobili w klasycznej klasie!

Obsługa wyjątków za pomocą programowania aspektowego (Postsharp)

O programowaniu aspektowym kiedyś już pisałem więc jeśli od strony teoretycznej nie jest to jasne to zachęcam do poszperania na blogu. Dzisiaj zaprezentuje framework Postsharp w wersji express (darmowa edycja, również do zastosowań komercyjnych). Jak wiemy, obsługa wątków czy wykonanie logów mogą być problemami cross-cutting. Postsharp jest typowym framework’iem implementującym AoP Zaczynamy od instalacji z NuGet:

image

 

Pomimo, że Postsharp express jest w pełni darmowy, musimy zarejestrować się i uzyskać klucz:

image

 

Następnie możemy zdefiniować nasz pierwszy aspekt, który uruchomi się w momencie wyrzucenia wątku. Generalnie w Postsharp istnieje kilka klas bazowych dla aspektów. W naszym przypadku skorzystamy z OnMethodBoundaryAspect, która zawiera kilka ciekawych metod. Implementacja aspektu polega po prostu na dziedziczeniu z OnMethodBoundaryAspect i przeładowaniu kilku metod. Zaglądając do dokumentacji dowiemy się trochę więcej o OnMethodBoundaryAspect:

[SerializableAttribute]
public abstract class OnMethodBoundaryAspect : MethodLevelAspect, 
    IOnMethodBoundaryAspect, IMethodLevelAspect, IAspect

Aby zobaczyć jakie metody możemy przeładować i kiedy są one wywołane przyjrzyjmy się następującemu pseudo-kodowi:

int MyMethod(object arg0, int arg1)
{
  OnEntry();
  try
  {
    // Original method body. 
    OnSuccess();
    return returnValue;
  }
  catch ( Exception e )
  {
    OnException();
  }
  finally
  {
    OnExit();
  }
}

Innymi słowy, mając metodę MyMethod, OnEntry zostanie wywołane przy wejściu do niej, OnSucces po wykonaniu logiki w niej zawartej, OnException w przypadku wyjątku, a OnExit zawsze przy wyjściu (niezależnie czy był wyjątek czy nie).

W naszym przypadku będziemy potrzebować tylko OnException. Zaimplementujmy więc pierwszy aspekt (zmodyfikowany przykład z oficjalnej dokumentacji Postsharp):

[Serializable]
public class ExceptionAspect : OnMethodBoundaryAspect
{
   private string methodName;
   
   public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
   {
       Debug.Assert(method.DeclaringType != null, "method.DeclaringType != null");
       methodName = method.DeclaringType.FullName + "." + method.Name;
   }
   public override void OnException(MethodExecutionArgs args)
   {
       Trace.Unindent();

       StringBuilder stringBuilder = new StringBuilder(1024);

       // Write the exit message. 
       stringBuilder.Append("Error:");
       stringBuilder.Append(this.methodName);
       stringBuilder.Append('(');

       // Write the current instance object, unless the method 
       // is static. 
       object instance = args.Instance;
       if (instance != null)
       {
           stringBuilder.Append("this=");
           stringBuilder.Append(instance);
           if (args.Arguments.Count > 0)
               stringBuilder.Append("; ");
       }

       // Write the list of all arguments. 
       for (int i = 0; i < args.Arguments.Count; i++)
       {
           if (i > 0)
               stringBuilder.Append(", ");
           stringBuilder.Append(args.Arguments.GetArgument(i) ?? "null");
       }

       // Write the exception message. 
       stringBuilder.AppendFormat("): Exception ");
       stringBuilder.Append(args.Exception.GetType().Name);
       stringBuilder.Append(": ");
       stringBuilder.Append(args.Exception.Message);

       // Finally emit the error. 
       Console.WriteLine(stringBuilder.ToString());
   }
} 

Metoda CompileTimeInitialzie jest wykonywana zawsze gdy aspekt jest doczepiony do konkretnej metody. Następnie w OnException pobieramy przydatne informacje takie jak nazwa metody, lista przekazanych parametrów czy stacktrace. Myślę, że możliwość wyprintowania parametrów i ich wartości jest niesłychanie przydatna. Oprócz tego, że będziemy wiedzieć gdzie wyjątek został wyrzucony, aspekt dostarczy nam informacji o konkretnym scenariuszu.

To nie koniec. Musimy zdefiniować zasady aplikowania aspektu. Najpierw spróbujmy doczepić aspekt do konkretnej, pojedynczej metody np.:

internal class Program
{
   private static void Main()
   {
       MethodCausingError("Witaj swiecie.");
   }   
   [ExceptionAspect]
   private static void MethodCausingError(string text)
   {
       throw new ArgumentException("Jakis blad.");
   }
}

Na ekranie, zobaczymy przechwycony wyjątek, wraz z parametrami:

image

 

W praktyce jednak, dużo lepiej skorzystać z bardziej globalnego mechanizmu tzn.:

[assembly: ExceptionAspect(AttributeTargetTypes = "*")] 

Powyższa linijka kodu zaaplikuje aspekt na każdej metodzie. Można również określić filtr, np. poprzez zdefiniowanie namespace:

[assembly: OurLoggingAspect(AttributeTargetTypes= 
"OurCompany.OurApplication.Controllers.*")] 

jeśli jest to nie wystarczające to można określić również widoczność typu (private, public itp.) czy typ metody:

[assembly: OurLoggingAspect 
(AttributeTargetTypes="OurCompany.OurApplication.Controllers.*",  
AttributeTargetTypeAttributes = MulticastAttributes.Public)]
// II
[assembly: OurLoggingAspect 
(AttributeTargetTypes="OurCompany.OurApplication.Controllers.*",  
AttributeTargetMemberAttributes = MulticastAttributes.Virtual)] 

W zasadzie warto na koniec zaznaczyć, że dla tego przykładu lepiej byłoby skorzystać z OnExceptionAspect zamiast OnMethodBoundaryAspect ponieważ wystarczy np. wyłącznie metoda OnException. To tylko takie wprowadzenie, w następnych wpisach pokażę bardziej szczegółowo sterowanie przepływem metod.