Warning: strpos() [function.strpos]: Empty needle in /home/dealer/kasat/pkg/cahir/web/html/lib/plugins/translation2/action.php on line 53

Warning: Cannot modify header information - headers already sent by (output started at /home/dealer/kasat/pkg/cahir/web/html/lib/plugins/translation2/action.php:53) in /home/dealer/kasat/pkg/cahir/web/html/inc/actions.php on line 163
erlang:concurrent [Krystian Bacławski Wiki]
 

Model aktorów

Omówienie

Podstawowe informacje o modelu aktorów

Co to jest aktor?

Aktor jest jednostką obliczeniową, która może wykonywać poniższe informacje:

  • utworzyć skończoną liczbę aktorów,
  • wysłać skończoną ilość komunikatów do aktorów,
  • określić akcję, która ma się wykonać po odebraniu następnego komunikatu.

Wszystkie z powyższych operacji mogą się wykonać w dowolnej kolejności.

Formalny opis modelu aktorów

  • Prawa dla systemów aktorów.
  • Semantyka operacyjna.
  • Semantyka denotacyjna.
  • Semantyka zmiany stanu (tranzytywna).

Model aktorów jest mocno powiązany z rachunkiem π.

Jak wygląda świat według aktora?

W jaki sposób aktor komunikuje się ze światem zewnętrznym?:

  • Aktorzy działają równolegle – komunikacja jest asynchroniczna.
  • Nie ma dzielenia stanu między aktorami.
  • Komunikacja odbywa się przy pomocy komunikatów.
  • Aktor nie ma wpływu na kolejność pojawiania się komunikatów adresowanych do niego.

Możliwość bezpośredniej komunikacji aktorów jest możliwa tylko wtedy, kiedy możemy jakoś ich identyfikować.

  • Każdy aktor posiada i zna własny adres.
  • Po utworzeniu aktora, aktor rodzic zna adres aktora potomka.
  • Adresy można przekazywać w komunikatach.
  • Aktor może wysłać komunikat tylko do aktorów, których zna.

Semantyka przesyłania komunikatów

Odbieranie komunikatów:

  • Kolejność odbierania pakietów może być różna od kolejności wysyłania.
  • Każdy aktor może posiadać bufor komunikatów wejściowych (skrzynkę pocztową).
  • W szczególności aktor może nie buforować komunikatów lub mieć skończony bufor – blokada wysyłającego, albo błąd wysyłania
  • Aktor nie musi powiadamiać czy odebrał komunikat.

Brak globalnego stanu!:

  • Nieograniczony niedeterminizm – dane żądanie zostanie spełnione, ale z nieustalonym opóźnieniem.
  • Lokalność – aktor może wysyłać tylko do tych, których zna po adresie.
  • Aktorzy są zorganizowani w sieć.
  • Dzięki przesyłaniu adresów topologia może się zmieniać.
  • Nie ma rozgłaszania, ale może być globalny system nazywania aktorów.

Porównanie z innymi paradygmatami

Paradygmat obiektowy, a model aktorów

Zamieniamy obiekty na aktorów! Każdy aktor jest obiektem, który:

  • zawiera wyłącznie swój stan (czyli wszystko co mu do życia potrzebne),
  • działa równolegle (ang. active object).

Zamieniamy wywołanie metod na wysłanie i odbierane komunikatów:

  • Wszystkie wywołania metod na obiekcie stają się asynchroniczne.
  • Komunikat zostaje zamieniony na wywołanie funkcji wewnątrz aktora.
  • Aktor decyduje jaką funkcjonalność udostępnić w ramach obsługi danego komunikatu.

Dziedziczenie zastępujemy implementacją zachowania:

  • Jednak czasem się przydaje.
  • Dziedziczenie obiektowe – jeśli coś jest typu B i dziedziczy po A, to jest też typu A.
  • Dziedziczenie behawioralne – jeśli coś jest typu B i zachowuje się jak A, to można to traktować jak A. Charakterystyczne dla języków z dynamicznym typowaniem – tzw. duck typing.

Procesy - Procesy w Erlangu

Wprowadzenie

Procesy i identyfikatory

Tworzenie:

Pid = spawn(Module,Function,Args)
  • Powyższe polecenie tworzy nowy proces i nakazuje mu wykonanie funkcji apply z podanymi argumentami.
  • Pid jest typem i przechowuje unikalny identyfikator procesu.

Uwaga:

  • Args jest zawsze listą.
  • Nowy proces zawsze zostanie utworzony, nawet jeśli argumenty spawn są nieprawidłowe!

Operowanie na procesach

Przykłady:

1> Pid1 = self().
<0.35.0>
2> is_pid(Pid1).
true
3> Pid1 = pid(0,35,0).
<0.35.0>
3> processes().
[<0.0.0>,<0.3.0>,<0.5.0>,...,<0.35.0>]
4> i().
Pid         Initial Call        Heap     Reds Msgs
Registered  Current Function   Stack
<0.0.0>     otp_ring0:start/2    987     2495    0
init        init:loop/1            2
...
5> spawn(mod_none,fun_none,[]).
<0.42.0>
 
=ERROR REPORT==== 30-Mar-2010::14:27:16 ===
Error in process <0.42.0> with exit value:
  {undef,[{mod_none,fun_none,[]}]}

Atrybuty procesu

Słownik procesu

Czym jest słownik procesu?

Odpowiednik Thread Local Storage. Każdy proces erlangowy ma przypisany prywatny słownik. Każdy klucz w tym słowniku może mieć tylko jedną wartość.

Uwaga! Nie zaleca się korzystania ze słowników! Dlaczego?

Przykład odwoływania się do prywatego słownika:

> put( x, an_atom ).
> get( x ).
an_atom
> put( y, "foobar" ).
> put( z, 10 ).
> get().
[{z,10},{y,"foobar"},{x,an_atom}]
> erase( y ).
"foobar"
> get().
[{z,10},{x,an_atom}]
> erase().
[{z,10},{x,an_atom}]
> get().
[]

Komunikacja

Wysyłanie komunikatów

Składnia:

Pid ! Message

Operator ! wiąże od prawej, więc można robić coś takiego:

Pid1 ! Pid2 ! Pid3 ! Message

Co jest równoważne:

Pid1 ! (Pid2 ! (Pid3 ! Message))

Semantyka:

  • Odbiorca buforuje komunikaty w skrzynce pocztowej (kolejka FIFO).
  • Wysyłanie jest asynchroniczne – nie blokuje.
  • Wysyłanie nigdy nie generuje błędu – jeśli odbiorca nie istnieje komunikat trafia do /dev/null.

Przykłady wysyłania komunikatów

Przykłady:

1> Pid = self().
<0.35.0>
2> Pid ! foobar.
foobar
3> Pid ! Pid ! hello.
hello
4> flush().
Shell got foobar
Shell got hello
Shell got hello
ok
5> 1/0.
** exception error: bad argument in an arithmetic expression
 in operator  '/'/2
called as 1 / 0
6> self().
<0.43.0>
7> pid(0,35,0) ! foobar.
foobar
8> flush().
ok

Odbieranie komunikatów

Składnia:

receive
  Pattern1 when Guard1 ->
    ExprBlock1;
  ...
  PatternK when GuardK ->
    ExprBlockK
after
  Timeout ->
    TimeoutExprBlock
end

Semantyka:

  • Klauzula kończy się wykonywać jeśli jeden z komunikatów się dopasuje.
  • Przeglądanie skrzynki zaczyna się od najstarszego komunikatu.
  • Jeśli dopasuje się, to odpowiednie wyrażenie zostanie wykonane.
  • Jeśli nie dopasuje się, to rozpatrywany jest następny komunikat.
  • Po czasie Timeout ms od wejścia do klauzuli receive wyrażenie TimeoutExprBlock zostanie wykonane i klauzla receive kończy swoje działanie.

Przykład odbierania komunikatów

Proces echo – odbijanie komunikatów:

-module(echo).
-export([go/0, loop/0]).
 
go() ->
  Pid = spawn(echo, loop, []),
  Pid ! {self(), hello},
  receive
    {Pid, Msg} ->
      io:format("~w~n",[Msg])
  end,
  Pid ! stop.
 
loop() ->
  receive
    {From, Msg} ->
      From ! {self(), Msg}, loop();
    stop ->
      true
  end.
 
2> echo:go().
hello
stop

Rejestrowanie procesów

Rejestrowanie procesów

Rejestrowanie procesów to nic innego jaki ich nazywanie – w tym przypadku atomami. Motywacja jest podobna jak w sieciach komputerowych:

  • posługiwanie się PID może być niewygodne,
  • chcemy znać proces o podanej funkcjonalności po nazwie,
  • a co jeśli proces się wywali – aktualizować wszędzie PID?

Nazywanie jest jakby prostszą wersją DNS.

Rejestrowanie:

  • Po utworzeniu procesu przypisujemy mu nazwę w postaci atomu. Jeśli atom jest przypisany do istniejącego procesu to się nie uda.
  • Po zakończeniu proces jest automatycznie wyrejestrowywany.
  • Zmienia się semantyka wysyłania komunikatów – można wysyłać bezpośrednio do atomu, który da się przekształcić w danej chwili przy pomocy funkcji whereis/1.

Proces odmierzający interwał czasu

-module(clock).
-export([start/2, stop/0]).
 
start(Time, Fun) ->
  register(clock, spawn(fun() -> tick(Time, Fun) end)).
 
stop() ->
  clock ! stop.
 
tick(Time, Fun) ->
  receive
    stop ->
      ok
  after Time ->
    Fun(),
    tick(Time, Fun)
  end.

Interakcja z zarejestrowanym procesem

> clock:start(5000, fun() -> io:format( "Tick ~p~n", [erlang:now()] ) end).
true
> whereis(clock).
<0.73.0>
> registered().
[...,clock,...]
Tick {1271,147664,846835}
Tick {1271,147669,852178}
Tick {1271,147674,857667}
Tick {1271,147679,860217}
Tick {1271,147684,861061}
Tick {1271,147689,864807}
Tick {1271,147694,869295}
Tick {1271,147699,870050}
Tick {1271,147704,871070}
> clock:stop().
stop
> unregister(clock).
** exception error: bad argument
 in function  unregister/1
called as unregister(clock)
> whereis(clock).
undefined

Szablon procesu

Jak powinien wyglądać interfejs procesu?

Interfejs procesu: Potrzebujemy jakiegoś ogólnego szablonu względem, którego będziemy konstruować procesy. Spróbujmy określić co jest przydatne w każdym procesie.

  • Chcielibyśmy mieć moduł, w którym zawrzemy wszystkie procedury procesu.
  • Nasz proces powinien być nazwany i zainicjowany danymi.
  • Po starcie powinien zarezerwować wszystkie niezbędne mu zasoby.
  • Powinien przechowywać swój stan wewnętrzny i w pętli realizować zlecenia.
  • Powinno się wywoływać pewne jego funkcjonalności przez interfejs, a nie bezpośrednio.
  • Chcielibyśmy umieć go zakończyć w taki sposób by prawidłowo zakończył swoje działanie i zwolnił wszystkie zasoby.

Przykładowy szablon procesu

-module(template).
-export([start/2, stop/1, call/2]).
-export([init/1]).
 
start(Name, Data) ->
  Pid = spawn(?MODULE,init,[Data]),
  register(Name, Pid), ok.
stop(Name) ->
  Name ! {stop, self()},
  receive {reply, Reply} -> Reply end.
call(Name, Msg) ->
  Name ! {request, self(), Msg},
  receive {reply, Reply} -> Reply end.
reply(To, Msg) -> To ! {reply, Msg}.
init(Data) -> loop(initialize(Data)).
loop(State) ->
  receive
    {request, From, Msg} ->
      {Reply,NewState} = handle_msg(Msg, State),
      reply(From, Reply), loop(NewState);
{stop, From} ->
   reply(From, terminate(State))
  end.
initialize(...)     -> ...
handle_msg(...,...) -> ...
terminate(...)      -> ...

Obsługa błędów - Przesyłanie informacji o błędach

Proces zakończył się i co dalej?

Jakiej obsługi błędów potrzebujemy?

Podejście do obsługi błędów:

  • Błędy się zdarzają. Ludzką naturą jest się mylić. Sprzęt potrafi zawieść. Awarie są nieuniknione.
  • Zamiast próbować za wszelką cenę ich unikać, po prostu trzeba na nie reagować. Po co programować defensywnie? Nie wiadomo kiedy obsługa danego błędu się przyda i czy o jakimś błędzie nie zapomnieliśmy. Napiszemy się – trzeba to jeszcze przetestować, a zostanie użyte raz od wielkiego święta.
  • Lepiej pozwolić wywalić się procesowi wcześniej. Proces się wywala kiedy operuje na niewłaściwych danych. Im dłużej będzie działał z niepoprawnymi danymi tym większa szansa, że będzie je propagował. Im szybciej się wywali tym mniej szkód poczyni.
  • Zajęcie awarią pozostawić innemu procesowi. Po co proces ma się zajmować awariami, które w nim wystąpią. Niech inny proces to robi – niech ma zaimplementowaną politykę działania.

Co daje nam Erlang?

Mechanizmy przesyłania błędów:

  • Łącze - niejawny kanał komunikacji między procesami do przesyłania informacji o błędach. Każdy proces ma zbiór sąsiadów, z którymi jest połączony takim kanałem. Jeśli proces wygeneruje błąd, to zostanie on rozgłoszony do sąsiadów.
  • Sygnał błędu - to informacja o błędzie, który zostaje wygenerowany przez proces przy zakończeniu działania. Sygnał błędu zawiera informacje o przyczynie jego wystąpienia. Przyczyną zakończenia procesu może być:
    • normalne zakończenie – oznaczane atomem normal,
    • niejawny błąd – występujący np przy próbie dopasowania, etc.
    • jawny błąd – zasygnalizowany przy pomocy funkcji wbudowanej exit.
  • Proces systemowy - to proces, który przetwarza sygnały błędów nadchodzące z procesów, z którymi jest połączony. Normalny proces po otrzymaniu sygnału błędu zostaje zakończony. W przypadku procesu systemowego błąd konwertowany jest do krotki {'EXIT',Pid,Reason} i wkładany do skrzynki pocztowej procesu. Aby proces stał się systemowym należy włączyć w nim wyłapywanie sygnałów.

Łącza

Strategia obsługi błędów

Domyślna reakcja na błędy:

Systemowy Sygnał Akcja
tak / nie kill Zakończ się. Rozgłoś do sąsiadów sygnał killed.
tak Reason Dodaj {'EXIT',Pid,Reason} do skrzynki.
nie normal Kontynuuj pracę. Sygnał zostaje porzucony.
nie Reason Zakończ się. Rozgłoś do sąsiadów sygnał Reason.

Jedynym sygnałem, którego nie da się przechwycić jest kill. Używa się go do zabijania opornych procesów.

Powiązane funkcje:

  • spawn_link odmiana spawn – atomowo tworzy proces i ustanawia łącze,
  • process_flag(trap_exit, true) włącza przechwytywanie sygnałów. Bieżący proces staje się systemowym,
  • link(Pid) tworzy dwukierunkowe łącze między bieżącym procesem, a procesem o identyfikatorze Pid,
  • unlink(Pid) anuluje łącze,
  • exit(Reason) powoduje zakończenie procesu z podanego powodu,
  • exit(Pid,Reason) powoduje wysłanie do procesu Pid informacji o zakończeniu bieżącego procesu z powodu Reason – symulacja zakończenia.

Monitorowanie

Wprowadzenie

Motywacja:

  • Łącza są nieprzezroczyste – po utworzeniu potencjalnie zmieniają zachowanie istniejących procesów.
  • Jeśli chcemy obserwować proces, a obserwator zginie, to skutkiem będzie niepotrzebne zabicie obserwowanego procesu.
  • Potrzebujemy jedynie jednokierunkowego łącza – co wtedy?

Monitor jest specjalnym jednokierunkowym łączem między procesami. Monitor identyfikuje się po referencji. Jeśli monitorowany proces zakończy się proces monitorujący dostanie do swojej skrzynki komunikat w postaci {'DOWN',Reference,process,Pid,Reason}.

Korzystanie

Powiązane funkcje:

  • spawn_monitor odmiana spawn – atomowo tworzy proces i ustanawia dla niego monitor,
  • erlang:monitor(process,Pid) bieżący proces staje się monitorem dla procesu Pid, zwraca referencję określającą akcję monitorowania,
  • erlang:demonitor(Reference) anuluje daną akcję monitorowania,
  • erlang:demonitor(Reference,[flush]) anuluje daną akcję monitorowania i upewnia się, że ze skrzynki zostały usunięte nieodebrane komunikaty związane z tą akcją.
 
erlang/concurrent.txt · Last modified: 2010/05/07 23:33 by Krystian Bacławski
 
Except where otherwise noted, content on this wiki is licensed under the following license:CC Attribution-Noncommercial-No Derivative Works 3.0 Unported
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki