Category Archives: C#

Trace vs Debug

W .NET istnieją dwie przydatne klasy do logowania wszelkich informacji: Trace oraz Debug. Często różnica nie jest jasna oraz klasy są ze sobą mylone. Co się dokładnie dzieje, gdy napiszemy następujący fragment kodu?

   Trace.WriteLine("Trace test");
   Debug.WriteLine("Debug test");

Efekt wydaje się podobny, w okienku debug zobaczymy wykonane logi:

1

Najlepiej zajrzeć do źródeł powyższych klas. Debug.WriteLine wygląda następująco:

   [System.Diagnostics.Conditional("DEBUG")]        
   public static void WriteLine(string message) 
   {
        TraceInternal.WriteLine(message);
   }

Z kolei Trace.WriteLine:

        [System.Diagnostics.Conditional("TRACE")]
        public static void WriteLine(string message) {
            TraceInternal.WriteLine(message);
        }

Pozostałe metody Trace oraz Debug wyglądają analogicznie. Wszystkie z nich (zarówno Debug jak i Trace) korzystają z TraceInternal. Jedyna różnica to atrybut Conditional. Nie trudno domyślić się, że Debug będzie widoczny tylko w trybie Debug. Jeśli przejdziemy do Release, Debug.WriteLine nie zostanie skompilowany czyli nie zobaczymy nic na wyjściu. Z kolei symbol “TRACE” jest domyślnie zawsze włączany w proces kompilacji, co możemy sprawdzić we właściwościach projektu:

2

Warto również zauważyć, że za pomocą obydwu klas można zapisywać logi w dowolnych miejscach, również w plikach czy bazach danych. Wystarczy odpowiednio skonfigurować Listeners:

ConsoleTraceListener myWriter = new ConsoleTraceListener();
Trace.Listeners.Add(myWriter);

AKKA.NET – Przykład obsługi błędów

W poprzednim wpisie pokazałem, w jaki sposób możemy zaprojektować obsługę błędów. Jak widać mamy do dyspozycji sporo opcji. Z punktu widzenia AKKA.NET nie jest to jednak tak skomplikowane. Wystarczy przeładować jedną metodę i zwrócić odpowiedni obiekt.

Tak jak w poprzednim wpisie będziemy testować kod na następującym “systemie”:

controller

Dla przypomnienia nasz ApplicationUserActor wygląda następująco:

public class ApplicationUserActor : UntypedActor
    {
        private readonly string _userName;

        public ApplicationUserActor(string userName)
        {
            _userName = userName;
        }

        protected override void OnReceive(object message)
        {
            Console.WriteLine("Received by {0}: {1}", _userName, message);
        }

        protected override void PreStart()
        {
            Console.WriteLine("{0}: PreStart",_userName);

          
            base.PreStart();
        }

        protected override void PostStop()
        {
            Console.WriteLine("{0}: PostStop", _userName);
            base.PostStop();
        }

        protected override void PreRestart(Exception reason, object message)
        {
            Console.WriteLine("{0}: PreRestart", _userName);
            base.PreRestart(reason, message);
        }

        protected override void PostRestart(Exception reason)
        {
            Console.WriteLine("{0}: PostRestart", _userName);
            base.PostRestart(reason);
        }
    }

Póki co niewiele mamy tam kodu – głównie hooking, które pomogą nam w zrozumieniu propagacji błędów.
Zmodyfikujmy metodę OnReceived tak, aby zasymulować wyjątek:

        protected override void OnReceive(object message)
        {
            if (message.ToString() == "error")
                throw new ArgumentException();

            Console.WriteLine("Received by {0}: {1}", _userName, message);
        }

W celu zdefiniowania obsługi błędów wystarczy przeciążyć metodę SupervisorStrategy aktora zarządzającego. Jeśli chcemy więc obsłużyć wyjątek w ApplicationUserActor, wtedy węzeł zarządzający (rodzic) to ApplicationUserControllerActor. Kod:

        protected override SupervisorStrategy SupervisorStrategy()
        {
            return new OneForOneStrategy((exception) =>
            {
                if (exception is ArgumentException)
                    return Directive.Restart;

                return Directive.Escalate;
            });
        }

W przykładzie wybraliśmy strategię OneForOneStrategy, którą opisałem już w poprzednim wpisie. W skrócie, rodzeństwo węzła, który spowodował wyjątek, nie będzie odgrywało tutaj żadnej roli. Wystarczy, że przekażemy wyrażenie lambda, które określa co należy zrobić w zależności od typu wyjątku. W powyższym przykładzie restartujemy aktora. Tak jak napisałem w poprzednim poście, mamy cztery sposoby reakcji:

  public enum Directive
  {
    Resume,
    Restart,
    Escalate,
    Stop,
  }

W celu zaprezentowania efektu, stwórzmy dwóch aktorów i wyślijmy serię wiadomości:

            var system = ActorSystem.Create("FooHierarchySystem");
            IActorRef userControllerActor =
                system.ActorOf<ApplicationUserControllerActor>("ApplicationUserControllerActor");

            userControllerActor.Tell(new AddUser("Piotr"));
            userControllerActor.Tell(new AddUser("Pawel"));
            var actor1 = system.ActorSelection("/user/ApplicationUserControllerActor/Piotr");
            var actor2 = system.ActorSelection("/user/ApplicationUserControllerActor/Pawel");

            Console.ReadLine();
            actor1.Tell("Sample message I");
            Console.ReadLine();
            actor1.Tell("error");
            Console.ReadLine();
            actor1.Tell("Sample message II");

            Console.ReadLine();

1
Widzimy, że w momencie wystąpienia błędu, aktor został zrestartowany. Ze screenu również można zauważyć, że kolejne wiadomości zostaną przetworzone. Stan wewnętrzny został zrestartowany, ale nie kolejka wiadomości. W celu zademonstrowania, że stan wewnętrzny faktycznie jest wymazywany (ponieważ tworzona jest nowa instancja), dodajmy prywatne pole do klasy:

    public class ApplicationUserActor : UntypedActor
    {
        private readonly string _userName;
        private string _internalState;
      ...
    }

InternalState jest wyświetlany i ustawiany w OnReceive:

        protected override void OnReceive(object message)
        {
            Console.WriteLine("{0}:Internal State: {1}",_userName,_internalState);

            if (message.ToString() == "error")
                throw new ArgumentException();

            _internalState = message.ToString();

            Console.WriteLine("Received by {0}: {1}", _userName, message);
        }

Teraz widzimy, że po wystąpieniu wyjątku, InternalState będzie pusty:
2

Analogicznie, spróbujmy zmienić dyrektywę restart na resume:

        protected override SupervisorStrategy SupervisorStrategy()
        {
            return new OneForOneStrategy((exception) =>
            {
                if (exception is ArgumentException)
                    return Directive.Resume;

                return Directive.Escalate;
            });
        }

Po uruchomieniu programu, przekonamy się, że stan wewnętrzny nie jest usuwany:
3

Zmieńmy również strategię na AllForOneStrategy:

        protected override SupervisorStrategy SupervisorStrategy()
        {
            return new AllForOneStrategy((exception) =>
            {
                if (exception is ArgumentException)
                    return Directive.Restart;

                return Directive.Escalate;
            });
        }

Efekt będzie taki, że wszystkie węzły podrzędne zostaną zrestartowane:
4

Jeśli w jakimś aktorze nie zdefiniujemy strategii obsługi błędów, wtedy domyślna będzie użyta:

   protected virtual SupervisorStrategy SupervisorStrategy()
    {
      return SupervisorStrategy.DefaultStrategy;
    }

Domyślna strategia to z kolei OneForOneStrategy.
Warto również przyjrzeć się  innym przeciążeniom konstruktora, np.:

    ///
<summary>
    /// Applies the fault handling `Directive` (Resume, Restart, Stop) specified in the `Decider`
    ///                 to all children when one fails, as opposed to <see cref="T:Akka.Actor.OneForOneStrategy"/> that applies
    ///                 it only to the child actor that failed.
    /// 
    /// </summary>

    /// <param name="maxNrOfRetries">the number of times a child actor is allowed to be restarted, negative value means no limit,
    ///                 if the limit is exceeded the child actor is stopped.
    ///             </param><param name="withinTimeRange">duration of the time window for maxNrOfRetries, Duration.Inf means no window.</param><param name="localOnlyDecider">mapping from Exception to <see cref="T:Akka.Actor.Directive"/></param>
    public OneForOneStrategy(int? maxNrOfRetries, TimeSpan? withinTimeRange, Func<Exception, Directive> localOnlyDecider)
      : this(maxNrOfRetries.GetValueOrDefault(-1), (int) withinTimeRange.GetValueOrDefault(Timeout.InfiniteTimeSpan).TotalMilliseconds, localOnlyDecider, true)
    {
    }

Widzimy, że oprócz wspomnianego wyrażenia lambda, możemy określić maksymalną liczbę prób oraz przedział czasowy.

Code review: Async void

Zaczynamy od razu od kodu:

    class Foo
    {
        public async void DoAsync()
        {
            await Task.Factory.StartNew(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine("DoAsync...");
            });
        }
    }

Dlaczego powyższy kod jest bardzo niebezpieczny? Nigdy nie należy używać async w połączeniu z void.
Przede wszystkim, użytkownik takiego kodu nie ma możliwości kontroli nad stworzonym wątkiem. Nie wiadomo, kiedy powyższa metoda zakończy się. Wywołanie będzie zatem wyglądać następująco:

    class Program
    {
        static void Main(string[] args)
        {
            Foo foo = new Foo();
            foo.DoAsync();

            Console.WriteLine("End");
            Console.ReadLine();
        }
    }

DoAsync tworzy nowy wątek i wykonuje jakiś kod w osobnym wątku. Ze względu na to, że metoda nie zwraca wątku, nie można użyć await lub Wait(). Jedynie możemy domyślać się, że po pewnym czasie zadanie zostanie wykonane.

Kolejnym problemem jest brak możliwości przetestowania takiej metody. Skoro niemożliwe jest wyznaczenie momentu zakończenia zadania, napisanie poprawnego i stabilnego testu również nie jest łatwe.

Jeszcze większe problemy będziemy mieli w momencie wyrzucenia wyjątku:

    class Foo
    {
        public async void DoAsync()
        {
            await Task.Factory.StartNew(() =>
            {
                Thread.Sleep(2000);
                throw new ArgumentException();
            });
        }
    }

Załóżmy, że kod klienta wygląda następująco:

        static void Main(string[] args)
        {
            Foo foo = new Foo();
            try
            {
                foo.DoAsync();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }

            Console.WriteLine("End");
            Console.ReadLine();
        }

Powyższy kod nigdy nie złapie wyjątku. Ponieważ DoAsync tworzy nowy wątek i nie czeka na jego zakończenie, wyjątek zostanie wyrzucony bezpośrednio do SynchronizationContext. Wątek jest wyrzucany w losowe miejsce, gdzie nie mamy kontroli. Z tego wynika, że wyjątek spowoduje zakończenie procesu. Metoda async void, zatem może zakończyć działanie procesu (poprzez wyrzucenie wyjątku) bez możliwości jego wyłapania – to bardzo niebezpieczne.

Zdefiniujmy teraz klasę foo w prawidłowy sposób:

    class Foo
    {
        public async Task DoAsync()
        {
            await Task.Factory.StartNew(() =>
            {
                Thread.Sleep(2000);
                throw new ArgumentException();
            });
        }
    }

Dzięki temu, że zwracamy Task, możliwe jest użycie słowa kluczowego await:

       private static async Task TestAsync()
        {
            Foo foo = new Foo();
            try
            {
                await foo.DoAsync();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }

Wyjątek zostanie wyłapany tak jak tego spodziewamy się.
Prawie zawsze zatem należy zwracać Task albo Task<T> – nawet jeśli wersja synchroniczna była typu void. Wyjątek stanowią event handler’y. Wynika to z definicji delegaty EventHandler. Warto jednak zwrócić uwagę, że handler’ów nie testujemy bezpośrednio oraz nie ma sensu owijać ich w try-catch. Jakakolwiek obsługa błędów jest w wewnątrz handler’a. Z tego względu jest to dopuszczalne i bezpieczne.

Wyjątku nie stanowią testy jednostkowe. Poniższy test jest również błędny:

[Test]
public async void Should_...().
{
      await _sut.DoAsync();

      Assert....
}

Prawidłowy test to oczywiście:

[Test]
public async Task Should_...().
{
      await _sut.DoAsync();

      Assert....
}

Framework nUnit w pewnej wersji (nie pamiętaj dokładnie, możliwe, że w 2.6) wspierał async void. Autorzy musieli napisać sporo kodu, aby móc obsługiwać takie testy. Od wersji 3.0 jednak, ta funkcjonalność (która była trochę hack’iem) została usunięta. W wersji 3.0 wyłącznie testy async task są wspierane i mogą być uruchamiane.

AKKA.NET – przełączanie stanów

Zanim przejdziemy do konkretnych problemów, musimy poznać przynajmniej podstawowe elementy AKKA.NET. W poprzednim wpisie opisałem jak definiować wiadomości oraz aktorów. Dzisiaj przejdziemy do kolejnego, bardzo ważnego elementu – przełączanie stanów. Jest to podstawowy element zarówno w modelu aktor, jak i w jakichkolwiek maszynach stanów (FSM). W poprzednim przykładzie przelewu środków z jednego konta na drugie, użyliśmy właśnie przełączania stanów. Dla przypomnienia:

class TransferActor
{
    public void OnTransferMessageReceived(TransferMessage transferMessage)
    {
        ActorsSystem.GetActor(transferMessage.From).Send(new WithdrawMessage(transferMessage.Amount));
         
        Context.Become(AwaitFrom(transferMessage.From,transferMessage.To,transferMessage.Amount));
    }
 
    public void AwaitFrom(string from, string to, int amount)
    {
        ActorsSystem.GetActor(to).Send(new DepositMessage(amount));
        Context.Became(AwaitTo(transferMessage.From, transferMessage.To, transferMessage.Amount));
    }
}

Po otrzymaniu zapytania o przesłaniu pieniędzy, przełączamy się w stan AwaitFrom. Po otrzymaniu potwierdzenia, składamy depozyt i znów czekamy na potwierdzenie.

W AKKA.NET zmiana stanu odbywa się za pomocą metody Become:

    public class SampleActor : UntypedActor
    {
        protected override void OnReceive(object message)
        {
            Console.WriteLine("Otrzymano wiadomosc ze stanu I {0}",message);
            Become(GoToState2);
        }

        private void GoToState2(object message)
        {
            Console.WriteLine("Otrzymano wiadomosc ze stanu II {0}", message);
        }
    }

Po otrzymaniu pierwszej wiadomości, przełączamy się do stanu drugiego, reprezentowanego przez metodę GoToState2. Od tego momentu wszystkie wiadomości będą obsługiwane przez GoToState2, a nie OnReceive. Odpalmy zatem następujący kod:

            var system = ActorSystem.Create("JakasNazwa");
            var actor1 = system.ActorOf<SampleActor>();


            for (int i = 0; i < 10; i++)
            {
                actor1.Tell(i.ToString());
            }

Na ekranie zobaczymy najpierw tekst “Otrzymano wiadomosc ze stanu I”, a potem 9 razy “Otrzymano wiadomosc ze stanu II”.

Druga przydatna i alternatywna metoda to BecomeStacked. Podobnie jak Become służy do przełączania stanu. Tym razem jednak, stan będzie przechowywany na stosie, zatem będzie można go potem zdjąć i powrócić do poprzedniego. Przykład:

   public class SampleActor : UntypedActor
    {
        protected override void OnReceive(object message)
        {
            Console.WriteLine("Otrzymano wiadomosc ze stanu I {0}",message);
            BecomeStacked(GoToState2);
        }

        private void GoToState2(object message)
        {
            Console.WriteLine("Otrzymano wiadomosc ze stanu II {0}", message);
            UnbecomeStacked();
        }
    }

Po odpaleniu, na zmianę będziemy mieć stan I oraz II. Oczywiście nie jesteśmy ograniczeni wyłącznie do dwóch stanów.

Akka.net – pierwszy przykład

W ostatnich dwóch wpisach pokazałem zasady działania modelu aktor. W kolejnych postach będę korzystał już z Akka.net zamiast pseudokodu. Dzisiaj czysty opis podstaw API – bez konkretnego problemu do rozwiązania.

Akka.net można zainstalować w formie pakietu Nuget:

Install-Package Akka

Następnie definiujemy wiadomość za pomocą zwyklej klasy typu immutable:

    public class TransferMoney
    {
        public string From { get; private set; }
        public string To { get; private set; }

        public TransferMoney(string from,string to)
        {
            From = @from;
            To = to;
        }
    }

Każdy aktor powinien dziedziczyć np. po ReceiveActor:

    public class TransferMoneyActor : ReceiveActor
    {
        public TransferMoneyActor()
        {
            Receive<TransferMoney>(msg =>
            Console.WriteLine("Transferring money from {0} to {1}", msg.From, msg.To));
        }
    }

Za pomocą Receive definiujemy, jakie wiadomości chcemy obsługiwać. Powyższa klasa zatem będzie odbierać wiadomości typu TransferMoney.
W akka.net aktorzy egzystują w tzw. ActorSystem. Aktorzy z dwóch różnych systemów są od siebie odizolowani. Inicjacja nowego systemu jest prosta:

var system = ActorSystem.Create("JakasNazwa");

Jak wiemy, aktorzy należą do konkretnych systemów, zatem w celu stworzenia aktora należy:

var actor1 = system.ActorOf<TransferMoneyActor>();

Wysłanie wiadomości odbywa się za pomocą metody Tell. Całość wygląda więc następująco:

var system = ActorSystem.Create("JakasNazwa");

var actor1 = system.ActorOf<TransferMoneyActor>();

actor1.Tell(new TransferMoney("nadawca", "odbiorca"));

Z poprzednich wpisów pamiętamy, że wiadomości są kolejkowane i wykonywane asynchronicznie. Stwórzmy zatem następującego aktora:

   public class TransferMoneyActor : ReceiveActor
    {
        public TransferMoneyActor()
        {
            Receive<TransferMoney>(DoWork,shouldHandle:null);
        }

        private void DoWork(TransferMoney msg)
        {
            Console.WriteLine("{0}:{1}",DateTime.Now,Thread.CurrentThread.ManagedThreadId);

            Thread.Sleep(5000);
        }
    }

Jeśli faktycznie wiadomości są kolejkowane i wykonywane jedno po drugim, wtedy powinniśmy zawsze widzieć ten sam identyfikator wątku w odstępach dokładnie 5 sekund. W celu udowodnienia, że zadania są wykonywane asynchronicznie, w pętli wyślemy 10 wiadomości:

            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("{0}: Wysylanie wiadomosci nr {1}", DateTime.Now,i);
                actor1.Tell(new TransferMoney("nadawca", "odbiorca"));
            }

Jeśli są synchronicznie wykonywane, wtedy po wyświetleniu tekstu “Wysylanie wiadomosci nr…”, powinniśmy zobaczyć numer wątku. Jeśli z kolei wykonywane są asynchronicznie, tak jak tego spodziewamy się, wtedy metoda Tell powinna wyłącznie umieścić wiadomość w kolejce.

Screenshot potwierdzający założenia:

tasks

W następnym wpisie, zajmiemy się znów jakimś problemem wielowątkowym, który najpierw rozwiążemy w “klasyczny sposób” z użyciem blokad, a później za pomocą modelu aktor.

Wielowątkowość: przykład modelu aktor

W ostatnim wpisie przedstawiłem zasadę działania modelu aktor. Zachęcam do przeczytania poprzedniego wpisu ponieważ dzisiaj skupię się na przykładzie, a nie podstawach teoretycznych. Jeśli poprzedni wpis nie był do końca zrozumiały, zachęcam do przeanalizowania przykładu z tego wpisu i potem powrócenia do poprzedniego postu – wtedy myślę, że wiele zagadnień będzie prostsze w zrozumieniu.

Poniższe przykłady należy traktować jako pseudokod. Stanowią one szkic wzorca aktor, a nie jego implementację. Do implementacji w kolejnych wpisach będę używał akka.net, ale moim zdaniem najważniejsze jest zrozumienie zasad, a nie nauczenie się kolejnego framework’a. Z tego względu, bardziej będę skupiał się na rozwiązywaniu różnych problemów wielowątkowych (np.problem ucztujących filozofów), a nie dokumentacji API.

Załóżmy, że chcemy rozwiązać klasyczny problem przelewu pieniędzy z jednego konta na drugie. W najprostszej postaci, będziemy mieli następującą klasę:

     class BankAccount
     {
         private int _Balance;


         public void Deposit(int amount)
         {
             _Balance += amount;
         }

         public void Withdraw(int amount)
         {
             if (amount <= _Balance)
                 _Balance -= amount;
             else
                 throw new ArgumentOutOfRangeException();
         }
     }

Oczywiście powyższy kod nie jest thread-safe, dlatego należy użyć blokady:

     class BankAccount
     {
         private int _Balance;
         private object _sync=new object();

         public void Deposit(int amount)
         {
             lock(_sync)
             {
               _Balance += amount;
             }
         }

         public void Withdraw(int amount)
         {
             lock(_sync)
             {
                 if (amount <= _Balance)
                    _Balance -= amount;
                 else
                    throw new ArgumentOutOfRangeException();
             } 
         }
     }

Kolejne zadanie to przetransferowanie pieniędzy z jednego konta na drugie. Klasyczne, błędne rozwiązanie to:

lock(accountA)
{
   lock(accountB)
   {
        accountA.Withdraw(5);
        accountB.Deposit(5);
   }
}

Oczywiście powyższy kod zakończy się deadlock, jeśli w tym samym czasie będziemy chcieli przelać pieniądze z konta A do B oraz z B do A.  Prawidłowe rozwiązanie to np. sortowanie blokad, przedstawione tutaj.

Wróćmy jednak do wzorca aktor. Stanowi on po prostu wyższy poziom abstrakcji dla wątków. Z poprzedniego wpisu wiemy, że aktorzy komunikują się za pomocą wiadomości, tak jak np. instancje w systemie kolejkowym. Zdefiniujmy zatem dwie wiadomości, dla depozytu i wycofywania środków:

     public class DepositMessage
     {
         public int Amount { get; }

         public DepositMessage(int amount)
         {
             Amount = amount;
         }
     }

     public class WithdrawMessage
     {
         public int Amount { get; }

         public WithdrawMessage(int amount)
         {
             Amount = amount;
         }
     }

Proszę zauważyć, że są one immutable – zawsze chcemy uniknąć współdzielenia stanu między różnymi aktorami. Następnie aktor, będzie obsługiwał wiadomości w sposób asynchroniczny:

     class BankAccountActor
     {
         private int _balance;

         public void OnReceive(object message)
         {
             if (message is WithdrawMessage)
             {
                 var withdrawMessage = ((WithdrawMessage)message);
                 if (withdrawMessage.Amount <= _balance)
                     _balance -= withdrawMessage.Amount;
             }

             if (message is DepositMessage)
             {
                 _balance += ((DepositMessage)message).Amount;
             }
         }
     }

Metoda OnReceive będzie wywoływana przez framework, w momencie otrzymania konkretnej wiadomości. Jak wspomniałem, przypomina to klasyczny system kolejkowy, ale OnReceive zawsze MUSI być wykonywane jedno po drugim. Jeśli dwie wiadomości przyjdą w tym samym czasie, mamy zagwarantowane, że OnReceive nie będzie wykonywane równocześnie z dwóch różnych wątków. Obsługa zatem może wyglądać następująco:


while(true)
{
    var message = blockingCollection.Dequeue();
    actor.OnReceive(message);
}

Z tego względu, nie musimy umieszczać w tych metodach żadnych blokad (brak współdzielonego stanu).
Następnie chcemy mieć możliwość transferu środków z jednego konta do drugiego. Zdefiniujmy zatem kolejną wiadomość:

     class TransferMessage
     {
         public string From { get; }
         public string To { get; }
         public int Amount { get; }

         public TransferMessage(string from, string to, int amount)
         {
             From = @from;
             To = to;
             Amount = amount;
         }
     }

Aktorzy mogą tworzyć hierarchie, w której jeden aktor zarządza kolejnymi. W naszym przypadku będziemy mieli dwa typy aktorów: TransferMoneyActor oraz BankAccountActor. Pierwszy z nich służy do koordynowania przepływu środków.

Najpierw implementujemy obsługę wiadomości TransferMessage:

     class TransferActor
     {
         public void OnTransferMessageReceived(TransferMessage transferMessage)
         {
             ActorsSystem.GetActor(transferMessage.From).Send(new WithdrawMessage(transferMessage.Amount));
             
             Context.Become(AwaitFrom(transferMessage.From,transferMessage.To,transferMessage.Amount));
         }

W momencie otrzymania TransferMessage, zostanie wysłana wiadomość do aktora, który reprezentuje konto nadawcy. Pamiętajmy, że wszystkie operacje są asynchroniczne, zatem stanowią model “fire&forget”. TransferActor jednak musi dowiedzieć się, czy środki zostały prawidłowo zdjęte z konta nadawcy. Z tego względu, jedną z bardzo ważnych właściwości aktorów jest zmiana kontekstu. W powyższym przykładzie chcemy zmienić kontekst w tryb oczekiwania na odpowiedź od nadawcy. Służy zwykle do tego metoda “Become”. Aktor zatem staje się aktorem oczekującym na odpowiedź od nadawcy. Odpowiedź przyjdzie oczywiście w formie kolejnej wiadomości:

     class MoneyWithdrawn
     {
         public ActorRef ActorRef { get;  }
         public int Amount { get;  }

         public MoneyWithdrawn(ActorRef actorRef,int amount)
         {
             ActorRef = actorRef;
             Amount = amount;
         }
     }

Następnie w momencie potwierdzenia wycofania pieniędzy, możemy wysłać wiadomość w celu umieszczenia środków na innym koncie:

     class TransferActor
     {
         public void OnTransferMessageReceived(TransferMessage transferMessage)
         {
             ActorsSystem.GetActor(transferMessage.From).Send(new WithdrawMessage(transferMessage.Amount));
             
             Context.Become(AwaitFrom(transferMessage.From,transferMessage.To,transferMessage.Amount));
         }

         public void AwaitFrom(string from, string to, int amount)
         {
             ActorsSystem.GetActor(to).Send(new DepositMessage(amount));
             Context.Became(AwaitTo(transferMessage.From, transferMessage.To, transferMessage.Amount));
         }

Analogicznie, aktor przechodzi w kolejny stan, oczekiwania na potwierdzenie złożenia depozytu. Potwierdzenie przyjdzie w formie kolejnej wiadomości:

     class TransferActor
     {
         public void OnTransferMessageReceived(TransferMessage transferMessage)
         {
            ActorsSystem.GetActor(transferMessage.From).Send(new WithdrawMessage(transferMessage.Amount));
             
             Context.Become(AwaitFrom(transferMessage.From,transferMessage.To,transferMessage.Amount));
         }

         public void AwaitFrom(string from, string to, int amount)
         {
             ActorsSystem.GetActor(to).Send(new DepositMessage(amount));
             Context.Became(AwaitTo(transferMessage.From, transferMessage.To, transferMessage.Amount));
         }

         public void AwaitTo(string from, string to, int amount)
         {
             Context.Finished();
         }

Widzimy, że każda operacja jest atomowa (pod warunkiem, że przetwarzanie wiadomości nie jest współbieżne). To bardzo ważna cecha systemów opartych na aktorach – należy rozszerzać hierarchie o tyle poziomów, aby konkretne zadanie było łatwe w implementacji. Przez “łatwe” mam na myśli sytuację, w której nie musimy korzystać z blokad.

Model dla prostych problemów (takich jak powyższy) jest moim zdaniem zła praktyką i przykładem over-engineering’u. Dla bardziej skomplikowanych problemów, znacząco to ułatwia zapobiegnięcie zakleszczeniom. Tak jak wspomniałem, aktor to pewien poziom abstrakcji. Ta abstrakcja daje nam ogromne możliwości skalowania – od problemu rozwiązywanego współbieżnie na np. 4 procesorach do środowiska opartego na wielu komputerach połączonych w sieć. Jeśli aktor jest abstrakcyjny, nic nie stoi na przykładzie, aby umieścić go na osobnym komputerze i przesyłać wiadomości za pomocą TCP. Jak widać, można skalować rozwiązanie od jednego procesu po wiele usług webowych komunikujących się dowolnymi sposobami (HTTP, systemy kolejkowy, TCP itp.). Użycie prostej blokady jest dobre, ale nie posiada żadnej abstrakcji – ogranicza nas do jednego procesu.

Code Review: Metody asynchroniczne z async oraz oczekiwanie na rezultat

Coraz więcej API dostarcza asynchroniczne wersje metod. Niektóre z nich, idą o krok dalej i w ogóle nie posiadają synchronicznej wersji. Załóżmy, że zewnętrzna biblioteka ma następującą metodę:

 async Task<string> FetchDataAsync() {...}

Często jednak nie potrzebujemy korzystać z wersji async i tylko ona komplikuje sprawę. W powyższym przypadku moglibyśmy pokusić się o:

  string data=dataProvider.FetchDataAsync().Result;

  Console.WriteLine(data);

Niestety powyższy kod może być niebezpieczny i wywołać w niektórych sytuacjach deadlock. Jeśli nie wiemy, jak została zaimplementowana metoda FetchDataAsync bardzo łatwo popełnić błąd. Załóżmy, że ciało metody wygląda następująco:

    public async Task<string> FetchDataAsync()
    {
         await DoSomethingAsync();

         return "Hello World";
    }
    private Task DoSomethingAsync()
    {
         return Task.Run(() => Thread.Sleep(2000));
    } 

Kiedyś na blogu pokazywałem już podobny przykład odnoście wydajności async\await. Jeśli wywołujemy await, pobierany jest kontekst przed wejściem w nowy wątek. Dzięki temu, po zakończeniu wątku, czyli w momencie wyjścia z await, wiadomo jaki wątek powinien kontynuować operację. Wynika to z faktu, że w większości przypadków, użytkownik spodziewa się, że wątek przed await i po jest taki sam. Rozważmy kod:

string data = await GetInfo();
_textBox.Text = data;

Gdyby po wyjściu await, kontekst wykonywania nie przechodził w ten przed wywołaniem wątku, wtedy aktualizacja interfejsu nie powiodłaby się – zawsze należy go aktualizować z wątku głównego.

Wracając do przykładu z deadlock. W celu powrócenia do wątku głównego, po wywołaniu “await DoSomethingAsync();”, należy oczywiście uzyskać do niego dostęp. Problem w tym, że będzie on ciągle zajęty. Wywołanie “string data=dataProvider.FetchDataAsync().Result;” blokuje wątek główny do momentu zakończenia operacji. Operacja oczywiście nigdy nie zakończy się ponieważ czeka ona na na dostęp do wątku głównego blokowanego przez “Task.Result”.

Zachowanie różni się w zależności od typu aplikacji. W przypadku aplikacji konsolowej, nie ma kontekstu synchronizacyjnego (TaskScheduler) więc operacja nie zakończy się deadlock’iem. Jeśli piszemy np. aplikację ASP.NET lub WPF wtedy doświadczymy opisanych wyżej problemów.

Istnieje możliwość wyłączenia mechanizmu powracania do poprzedniego kontekstu. Służy do tego ConfigureAwait(false);

        public async Task<string> FetchDataAsync()
        {
            await DoSomethingAsync().ConfigureAwait(false);

            return "Hello World";
        }

Po wywołaniu ConfigureAwait(false) nie będziemy mieli problemów z deadlock, ponieważ żaden kontekst nie będzie pobierany. Oczywiście nie jest to eleganckie rozwiązanie. ConfigureAwait jest jednak bardzo przydatny ze względów wydajnościowych – np. gdy w pętli wywołujemy await, wtedy zwykle nie ma sensu pobierać kontekstu.

Jeśli zatem wywołujemy metodę async, powinniśmy używać await, inaczej istnieje duże ryzyko, że kod po prostu zawiesi się.

Atrybut InternalsVisibleTo dla blibliotek strong-named

Atrybut InternalsVisibleTo służy do definiowania zaprzyjaźnionych bibliotek. “Zaprzyjaźniona” biblioteka to taka, która ma dostęp do klas i metod z modyfikatorem “internal”. Zwykle korzysta się z niego w celu przetestowania wewnętrznych klas. Czasami bywa, że klasy w bibliotece mają modyfikator internal i co za tym idzie, nie ma do nich bezpośrednio dostępu z testów. Za pomocą InternalsVisibleTo można zrobić wyjątek dla jakieś biblioteki, w tym przypadku projektu z testami.

Wystarczy w pliku Assembly.cs biblioteki zawierającej klasy internal umieścić:

[assembly:InternalsVisibleTo("AnyName.Tests")

Od tego momentu, AnyName.Tests będzie mogło korzystać z wewnętrznych klas projektu,  w którym znajduje się powyższy atrybut.
Oczywiście należy pamiętać, żeby testy jednostkowe skupiały się na testowaniu zachowania, a nie internali. W wielu przypadkach, powyższy atrybut jest sygnałem, że testujemy nie to co trzeba. Czasami jednak, testowanie przez publiczne API może być zbyt skomplikowane i niedokładne. Osobiście używam czasami tego atrybutu, jeśli logika w klasach wewnętrznych jest zbyt bardzo skomplikowana, aby testować ją wyłącznie przez publiczne API.

Jeśli biblioteka jest  typu strong-named (podpis cyfrowy), wtedy musimy podać pełny klucz publiczny obok nazwy. Obydwie biblioteki (logika oraz testy) muszą być zatem podpisane. Wtedy obok nazwy, podajemy również klucz publiczny, na przykład:

[assembly:InternalsVisibleTo("TestCoverage.Tests,PublicKey=002400000480000094000000060200000024000052534131000400000100010085d32843e5e1f42acd023289
dacebe34befbf561bdbb163367bb727f9292824db5aac63c7e72e45e273809937050d21230653def915ecc91e87d1eb4313cc4ed7357fd61d7698790901d1134ba34a9ce0f82f3dfb0e9bad
9c3120a3a6324a333718636b232f4a0b41c72428f2d8704d2da83edc496fe2325816bc8dfdad8feae")]

Jak widzimy na powyższym przykładzie, wklejamy pełny klucz publiczny, a nie jego token. Wystarczy, że odpalimy Developer Command Prompt oraz użyjemy poniższej komendy:

sn -Tp TestCoverage.Tests.dll

PublicKey

Powyższa komenda wyświetli zarówno pełny klucz, jak i jego token.

Producent-konsument w C# – BlockingCollection

BlockingCollection jest specjalną kolekcją danych, przygotowaną do implementacji wzorca producent-konsument. Nakład pracy do implementacji tego wzorca jest minimalny z BlockingCollection. Nie musimy martwić się o synchronizację, sekcję krytyczną czy deadlock. Zacznijmy od razu od przykładu.
Producent będzie wyglądać następująco:

       private static void Produce(BlockingCollection<int> buffer)
        {
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("Producing {0}", i);
                Thread.Sleep(10);

                buffer.Add(i);
            }

            buffer.CompleteAdding();
        }

Jak widzimy, implementacja producenta to nic innego jak dodawanie danych do kolekcji. Metoda Add jest thread-safe więc nie musimy używać lock. Ponadto robi to bardzo optymalnie, ponieważ nie jest to prosty mechanizm polegający po prostu na wstawieniu lock’a. Zaglądając do implementacji Add, zobaczymy między innymi spinning. BlockingCollection należy to tzw. concurrent collections o których już pisałem na blogu. Oznacza to, że jakiekolwiek operacje są zaimplementowane w ten sposób, aby unikać blokad. Zostało to osiągnięte na poziomie projektu (np. kilka mini-kolekcji w środku dla różnych wątków), jak i używania spinningu, gdy wiadomo, że zbyt długo nie będzie trzeba czekać.

Metoda CompleteAdding kończy produkcję i konsumenci nie będą już dłużej czekać. Musimy ją wywołać na końcu ponieważ w przeciwnym wypadku, konsumenci będą myśleli, że produkcja ciągle trwa i należy wciąż czekać.
Przyjrzyjmy się teraz konsumpcji:

        private static void Consume(BlockingCollection<int> buffer)
        {
            foreach (var i in buffer.GetConsumingEnumerable())
            {
                Console.WriteLine("Consuming {0}.", i);
                Thread.Sleep(20);
            }
        }

Kluczem jest metoda GetConsumingEnumerable. W bezpieczny sposób usuwa one dane z bufora. Jeśli w buforze nic nie ma po prostu wątek będzie blokowany. Iterator zakończy dopiero działanie, gdy producent wywoła CompleteAdding. W przeciwnym wypadku, foreach będzie zdejmował dane z kolekcji, lub blokował wywołanie w oczekiwaniu na więcej danych. Jeśli zajrzyjmy do implementacji wewnętrznej, znowu znajdziemy semafory i SpinWait.

Tak naprawdę, do najprostszej implementacji nic więcej już nie potrzebujemy. Całość wygląda zatem następująco:

class Program
    {
        static void Main(string[] args)
        {
            var buffer=new BlockingCollection<int>();

            var producerTask = Task.Run(() => Produce(buffer));
            var consumeTask = Task.Run(() => Consume(buffer));

            Task.WaitAll(producerTask, consumeTask);
        }

        private static void Produce(BlockingCollection<int> buffer)
        {
            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("Producing {0}", i);
                Thread.Sleep(10);

                buffer.Add(i);
            }

            buffer.CompleteAdding();
        }

        private static void Consume(BlockingCollection<int> buffer)
        {
            foreach (var i in buffer.GetConsumingEnumerable())
            {
                Console.WriteLine("Consuming {0}.", i);
                Thread.Sleep(20);
            }
        }
    }

W praktyce jednak trzeba rozważyć kilka innych “drobiazgów”. Co jeśli wyjątek zdarzy się podczas konsumpcji danych? Producent wciąż będzie generował dane, co przecież zwykle nie ma sensu i spowoduje memory leak. Dlatego lepiej napisać obsługę błędów:

       private static void Consume(BlockingCollection<int> buffer)
        {
            try
            {
                foreach (var i in buffer.GetConsumingEnumerable())
                {
                    Console.WriteLine("Consuming {0}.", i);
                    Thread.Sleep(20);
                    throw new Exception();
                }
            }
            catch
            {
                buffer.CompleteAdding();
                throw;
            }
        }

W momencie wystąpienia błędu, wywołujemy CompleteAdding, co spowoduje, że próba dodania nowych danych przez producenta zakończy się wyjątkiem InvalidOperationException i zakończeniem produkcji.
Analogicznie, warto dostać klauzule try-finally w producencie:

        private static void Produce(BlockingCollection<int> buffer)
        {
            try
            {
                for (int i = 0; i < 100; i++)
                {
                    Console.WriteLine("Producing {0}", i);
                    Thread.Sleep(10);

                    buffer.Add(i);
                }
            }
            finally
            {
                buffer.CompleteAdding();
            }

        }

W przypadku producenta, chcemy wywołać CompleteAdding zarówno w przypadku powodzenia (aby konsument już dłużej nie czekał), jak i wyjątku.
CompleteAdding tak naprawdę korzysta z CancellationToken, który jest znany nam z TPL. Prawdopodobnie warto również dodać obsługę wyjątków InvalidOperationException, tak aby dwukrotnie nie wywoływać CompleteAdding.

Inna bardzo ważna obserwacja to przypadek, gdy konsument jest dużo wolniejszy niż producent. Załóżmy, że wyprodukowanie zajmuje jedną sekundę, a konsumpcja 10. W tym przypadku, po długim przetwarzaniu możemy mieć do czynienia z ogromną alokacją pamięci ponieważ producent będzie ciągle dodawał dane, a konsument nie nadąży z usuwaniem ich.

BlockingCollection w bardzo prosty sposób rozwiązuje ten problem poprzez wprowadzenie maksymalnego limitu “porcji” w kolekcji. Wystarczy, w konstruktorze przekazać maksymalną pojemność:

var buffer = new BlockingCollection<int>(boundedCapacity: 10);

Po przekroczeniu limitu, metoda Add nie wyrzuci wyjątku, a po prostu będzie blokowała wywołanie za pomocą wspomnianych wcześniej technik (spinning, locking etc).

Porównywanie znaków, ToUpper, string.IndexOf oraz StringComparison.Ordinal

Resharper daje naprawdę cenne wskazówki. Nie wszystkie są oczywiste i czasami należy zagłębić się w temat. Jedną z takich wskazówek jest używanie IndexOf wraz z StringComparison.Ordinal.

Załóżmy, że mamy następujący kod:

string text = "test";
Console.WriteLine(text.IndexOf("est"));

Resharper zasugeruje konwersję do:

string text = "test";
Console.WriteLine(text.IndexOf("est", StringComparison.Ordinal));

Dlaczego?
Jeśli nie przekażemy ustawień regionalnych jawnie, wtedy domyślnie aktualna zostanie użyta.
Czasami oczywiście dokładnie tego chcemy i dlatego domyślnie przekazywany jest StringComparison.CurrentCulture.

W niektórych sytuacjach może spowodować to bardzo irytujące problemy. Każdy język ma pewne zasady, które nie zawsze pokrywają się z intuicją osoby piszącącej kod. Najsłynniejszym chyba przykładem jest język turecki i litera ‘i’.

Czego byśmy spodziewali się po poniższym kodzie?

string text = "some text this";
Console.WriteLine(text.ToUpper().IndexOf("THIS")); 

Naturalne wydaje się, że po wywołaniu ToUpper, tekst “THIS” zostanie znaleziony (na pozycji 10). Zmieńmy kulturę na tr-TR (Turcja):

Thread.CurrentThread.CurrentCulture=new CultureInfo("tr-TR");
string text = "some text this";
Console.WriteLine(text.ToUpper().IndexOf("THIS"));

Na ekranie zobaczymy -1. W języku tureckim, wielka litera ‘i’ to İ, a nie ‘I’.

Inny przykład to w niemieckim litery ‘ß’ oraz ‘ss’, które będą traktowane jako takie same, jeśli ustawimy odpowiednio kulturę.

Ciekawy przykład, znalazłem również na StackOverflow:

//SOURCE: http://stackoverflow.com/a/10941507
var s1 = "é"; //é as one character (ALT+0233)
var s2 = "é"; //'e', plus combining acute accent U+301 (two characters)

Console.WriteLine(s1.IndexOf(s2, StringComparison.Ordinal)); //-1
Console.WriteLine(s1.IndexOf(s2, StringComparison.InvariantCulture)); //0
Console.WriteLine(s1.IndexOf(s2, StringComparison.CurrentCulture)); //0

Problem w tym, że nie mamy pojęcia na jakim komputerze nasz kod jest wykonywany. Musimy przyjmować, że może być to dowolna kultura. Jeśli zatem, porównujemy angielskie czy polskie znaki, wtedy nie chcemy korzystać z ustawień regionalnych. Co jeśli aplikacja webowa jest hostowana na niemieckim serwerze, a porównujemy w niej wyłącznie polskie słowa, bo np. baza danych i aplikacja jest wykorzystywana wyłącznie w Polsce. Z tego względu, bezpieczniejszą opcją jest StringComparison.Ordinal, która porównuje wartości liczbowe znaków (np. kod ASCII).