Programowanie równoległe w Erlangu, część 2

09.11.2010 - Marek Materzok
TrudnośćTrudność

W poprzednim artykule dowiedzieliśmy się, jak pisać programy równoległe w Erlangu przy użyciu procesów i wymiany komunikatów. Rozwiniemy teraz tę wiedzę i nauczymy się, jak radzić sobie z błędami, jak wymieniać kod pracującego programu bez przerywania go i w jaki sposób uruchamiać procesy na zdalnych komputerach.

Obsługa błędów

Na poziomie procesów język Erlang posiada dość konwencjonalny mechanizm obsługi błędów oparty na wyjątkach. Informacja o błędzie - na przykład dzieleniu przez zero, nieprawidłowym dopasowaniu wzorca, czy też błędzie zgłoszonym przez programistę za pomocą instrukcji throw, jest propagowana wyżej, do momentu, aż zostanie obsłużona przez instrukcję catch lub try. Błąd nie obsłużony kończy działanie procesu.

Instrukcja try ma następującą postać:

throw(Term)

Komunikat o błędzie Term może być dowolnym termem. Podobnie, jak w przypadku komunikatów, najczęściej jako komunikatu o błędzie używa się krotki, której pierwszy element jest atomem oznaczającym rodzaj błędu - na przykład {blad_pliku, "Plik nie istnieje"}.

Prostą metodą obsługi błędów jest instrukcja catch. Wyrażenie postaci catch wyrażenie oblicza się do wartości wyrażenia wewnętrznego, jeśli te wykona się bez błędów, w przeciwnym wypadku wynikiem jest komunikat błedu. Przykładowo, następujące wyrażenie:

catch 2+2

Oblicza się do wartości 4, zaś takie wyrażenie:

catch 2+throw(blad)

Oblicza się do atomu blad.

Snippet icon Eksperymentuj! Napisz na przykład catch 1/0. albo catch a=b. (jak zwykle pamiętając o kropce na końcu!)

Z instrukcji catch nie korzysta się zbyt często, ponieważ jest kłopotliwa w użyciu. Częściej używaną, bo i bardziej wszechstronną, jest instrukcja try, pozwalająca obsłużyć konkretny rodzaj błędu określony za pomocą wzorca. Komunikaty błędów, które nie dopasowały się do żadnego wzorca, nie są przechwytywane przez instrukcję try. Domyślnie instrukcja try przechwytuje tylko błędy wywołane przez instrukcję throw. Dla przykładu, w poniższym kodzie:

test(X) ->
    try
        if
            X == 0 -> throw(zero);
            true -> 1/X
        end
    catch
        zero -> 42
    end.

Jeśli X było różne od zera, wynikiem jest liczba odwrotna, w przeciwnym wypadku wynikiem jest 42.

Za pomocą instrukcji try można, podobnie jak za pomocą catch, przechwytywać błędy wykonania programu. Powyższy program można lepiej zapisać w taki sposób:

test(X) -> try
    try
        1/X
    catch
        error:badarith -> 42
    end.

Jak widać, dzielenie przez 0 wywołuje błąd badarith. Najczęściej występujące rodzaje błędów to:

  • badarg - nieprawidłowy argument funkcji, na przykład spawn(1).
  • badarith - błąd w obliczeniach, na przykład 1/0.
  • {badmatch, V} - wartość V nie została dopasowana do wzorca przy instrukcji przypisania, na przykład {a, X} = {b, 1}.
  • function_clause - żadna z klauzul definiujących funkcję nie została dopasowana.
  • {case_clause, V} - wartość V nie została dopasowana do żadnego ze wzorców instrukcji case.
Snippet icon Napisz funkcję test(X), która wywołuje funkcję bledna(X) i zwraca wartość {ok, bledna(X)}, jeśli funkcja bledna wykona się bez błędów, wartość {throw, Blad} jeśli funkcja bledna wywołała throw(Blad), zaś {error, Blad}, jeśli nastąpi błąd wykonania Blad.

Co się dzieje, gdy błąd nie zostanie przechwycony przez żadną instrukcję catch ani try? Jak wspomniałem wcześniej, proces, który wywołał błąd, kończy pracę. Nie jest to najczęściej dobry pomysł, gdyż zakończony proces mógł komunikować się z innymi procesami - a inne procesy, nieświadome awarii swojego partnera, mogą niepotrzebnie długo czekać na komunikaty, które nigdy nie nadejdą. W związku z tym w Erlangu istnieje dodatkowy mechanizm pozwalający procesom informować inne procesy o swojej awarii - tak zwane powiązania. Procesy powiązane ze sobą w wyniku błędu kończą działanie wspólnie - jeśli w jednym z procesów nastąpi błąd, kończą działanie wszystkie z nich.

Ilustracja

Znając identyfikator innego procesu Pid, proces może utworzyć z nim powiązanie za pomocą funkcji link(Pid). Można też utworzyć powiązanie z nowym procesem w momencie jego tworzenia, używając funkcji spawn_link zamiast omówionej wcześniej spawn. Powiązanie usuwa się funkcją unlink(Pid).

Usuwanie powiązanych procesów w wypadku błędu jest najczęściej dobrą strategią - jeśli procesy blisko współpracowały ze sobą, awaria jednego z nich najczęściej uniemożliwi zakończenie pracy wszystkim! W niektórych sytuacjach lepiej jest jednak, aby powiązanego procesu nie usuwać, ale żeby mimo wszystko wiedział on o awarii swojego partnera - na przykład po to, aby uruchomić go ponownie. Można wtedy wywołać następującą funkcję:

process_flag(trap_exit, true)

Instrukcja ta powoduje, że w przypadku awarii powiązanego procesu aktualny proces nie zakończy działania, za to otrzyma komunikat postaci {'EXIT', Pid, Blad}, gdzie Pid jest identyfikatorem procesu kończącego działanie, a Blad - komunikatem błędu, który spowodował jego zakończenie. Proces działający w ten sposób nazywamy przechwytującym komunikaty o wyjściu. A oto przykładowy program, działający podobnie do poprzednich:

test(X) ->
    process_flag(trap_exit, true),
    Self = self(),
    Pid = spawn_link(fun () -> Self ! {ok, X/0} end),
    receive
        {ok, A} -> A;
        {'EXIT', Pid, {badarith, _}} -> A
    end.

Na koniec wspomnę jeszcze o funkcji exit(Pid, Blad). Wykonanie tej funkcji daje efekt analogiczny do awarii procesu powiązanego z procesem Pid - proces ten kończy działanie lub otrzymuje komunikat EXIT, w zależności od tego, czy przechwytuje komunikaty o wyjściu, czy nie. Jeśli jako Blad poda się kill, proces Pid zostanie zakończony nawet wtedy, gdy przechwytuje komunikaty o wyjściu.

0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com