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
.
|
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
.
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.
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.