Programowanie równoległe w Erlangu

22.07.2010 - Marek Materzok
TrudnośćTrudność

O pisaniu programów wykonujących wiele zadań równocześnie.

Świat jest równoległy

W każdej chwili jednocześnie następuje wiele różnych zdarzeń. Niektóre z nich są od siebie całkowicie niezależne - przykładowo, wschód Słońca nastąpi o ustalonej porze niezależnie od tego, o której godzinie zjesz śniadanie. Inne z kolei są uzależnione od siebie - na przykład przyspieszanie samochodu jest zależne od naciśnięcia pedału gazu przez kierowcę. Również w komputerach wiele akcji przebiega równolegle: jednocześnie uruchomionych jest wiele programów, które muszą jednocześnie komunikować się z użytkownikiem, przesyłać dane przez sieć, korzystać z dysku twardego, wykonywać obliczenia. Jeszcze lepiej równoległość widać na serwerach, gdzie z danej usługi korzystają często dziesiątki tysięcy użytkowników jednocześnie. Czasem wymogi wydajnościowe lub techniczne programu wymagają, aby pracował on jednocześnie na kilku różnych komputerach, pracujących w pewnym stopniu niezależnie od siebie. Taki rodzaj równoległości nazywamy rozproszeniem.

Mimo tego, że równoległość jest tak istotna, wsparcie dla niej ze strony powszechnie używanych języków programowania jest raczej nikłe. Programy pisze się jako sekwencję kroków, "najpierw zrób to, potem tamto", a w takim opisie nie ma miejsca na równoległość - trzeba więc ją w taki czy inny sposób "udawać". Tradycyjne rozwiązanie problemu polega na tym, że pozwalamy na istnienie pewnej liczby "wątków", jednocześnie korzystających ze wspólnej pamięci - jednak każdy, kto programował kiedyś w ten sposób, wie, że bardzo łatwo doprowadzić do subtelnych, trudnych do wyśledzenia błędów. Alternatywnym rozwiązaniem jest podzielenie programu na procesy, nie dzielące między sobą pamięci, za to potrafiące wymieniać między sobą komunikaty. Te dość naturalne podejście jest centralnym elementem zdobywającego coraz większą popularność języka programowania Erlang. W Erlangu tworzenie i usuwanie procesów oraz przesyłanie komunikatów jest wyjątkowo proste, co zachęca do tworzenia programów wykorzystujących równoległość.

Programowanie rozproszone również sprawia kłopoty. Najprostszym rozwiązaniem jest uruchomienie osobnej instancji programu na każdej maszynie i samodzielne zajmowanie się kwestiami rozproszenia - komunikacją, równoważeniem obciążenia, obsługą błędów. Takie podejście sprawdza się dla bardzo prostych zadań (na przykład, właśnie w ten sposób najłatwiej rozproszyć serwer stron WWW), jednak im bardziej złożone zadanie, tym więcej kłopotu. Można też skorzystać z zestawu narzędzi do programowania rozproszonego, jednak najczęściej są one bardzo skomplikowane. W Erlangu zaś tworzenie zdalnych procesów i komunikacja z nimi działa praktycznie tak samo jak odpowiedniki działające w obrębie jednej maszyny, ponadto przenoszenie zadań pomiędzy maszynami jest bezproblemowe.

Jednak zanim przedstawię, jak pisać programy równoległe w Erlangu, poświęcę trochę czasu na omówienie podstaw języka, ponieważ łącząc w sobie cechy Prologu i języków funkcyjnych może on robić wrażenie dość nietypowego.

Podstawy programowania w Erlangu

Jak w wielu językach, podstawowym elementem programu w Erlangu jest funkcja. Funkcje w Erlangu posiadają nazwę i pewną liczbę parametrów. Przykładowa funkcja, mnożąca parametr przez dwa, wygląda następująco:

razyDwa(X) -> X*2.
Snippet icon Napisz analogicznie funkcję kwadrat, podnoszącą liczbę do kwadratu!

Jak widać, treść funkcji piszemy po znaku strzałki ->, a kończymy kropką. Wynikiem funkcji jest wartość podanego wyrażenia, tutaj X*2. Warto zwrócić uwagę, że nazwa zmiennej jest zapisana wielką literą - w Erlangu nazwy zmiennych muszą rozpoczynać się od wielkiej litery! Inną ważną kwestią jest to, że może istnieć kilka funkcji o tej samej nazwie, ale z różną liczbą argumentów. Z pewnych powodów w Erlangu z tej możliwości korzysta się często, ale o tym później.

A co, jeśli chcielibyśmy zrobić w funkcji coś poza obliczeniem wyniku - na przykład wypisać coś na ekran? Bardzo proste - wystarczy rozdzielić kolejne działania przecinkiem:

wypiszIRazyDwa(X) -> io:write(X), X*2.

W przypadku, gdy treść funkcji składa się z kilku wyrażeń, wynikiem funkcji jest wartość ostatniego z nich.

Snippet icon Napisz analogicznie funkcję ikwadrat, wypisującą parametr, następnie wypisującą jej kwadrat i zwracającą jej kwadrat jako wynik.

Instrukcja warunkowa w Erlangu posiada dowolną liczbę "gałęzi", wykonana zostanie pierwsza, której warunek jest prawdziwy. Jak większość konstrukcji w Erlangu, instrukcja warunkowa jest wyrażeniem, a oblicza się do wartości wybranej "gałęzi". Zapisuje się ją następująco:

wartoscBezwzgledna(X) ->
    if
        X >= 0 -> X;
        true -> -X
    end.

Istotnym jest, że jeśli żadna z "gałęzi" nie zostanie wykonana, zostanie zgłoszony błąd - należy więc pamiętać o obsłużeniu wszystkich przypadków! Druga uwaga jest taka, że wyrażenia dopuszczalne w warunkach są dość ograniczone - w szczególności nie jest możliwe wywoływanie własnych funkcji, zatem takie wywołania należy wykonywać przed instrukcją warunkową.

Czasem zamiast instrukcji warunkowej stosuje się inną konstrukcję językową - klauzule funkcji. Otóż funkcje można definiować przez przypadki, a przy wywołaniu wykonana zostanie pierwsza klauzula, której warunek będzie prawdziwy. Poprzedni przykład można z pomocą klauzul przepisać tak:

wartoscBezwzgledna(X) when X >= 0 -> X;
wartoscBezwzgledna(X) -> -X.

Jak widać, funkcja zapisana w ten sposób jest dużo bardziej zwięzła, będziemy więc korzystać z tej konstrukcji dość często.

Snippet icon Napisz analogicznie funkcję signum, której wynikiem jest 1 dla liczb dodatnich, -1 dla ujemnych i 0 dla 0. Spróbuj użyć zarówno klauzul, jak i instrukcji warunkowej.

Instrukcję przypisania w Erlangu zapisuje się następująco:

razyDwaIWypisz(X) ->
    Y = X*2,
    io:write(Y),
    Y.

Instrukcja przypisania kryje w sobie niespodzianki. Otóż każdą zmienną można przypisać co najwyżej raz! Jest to dość powszechna cecha tak zwanych języków funkcyjnych, do których Erlang należy. Cecha ta wpływa w dość istotny sposób na strukturę pisanych programów, nie można bowiem używać pętli. Zamiast tego stosuje się rekursję - wywołanie przez funkcję samej siebie. Dla przykładu, funkcję silni można zaprogramować w następujący sposób:

silnia(X) when X == 0 -> 1;
silnia(X) -> X * silnia(X-1).

Istnieje jednak lepszy sposób na zaprogramowanie tej funkcji. Obliczając silnię za pomocą pętli, używa się dodatkowej zmiennej, w której zapisuje się częściowe wyniki. Rolę tej zmiennej może spełniać dodatkowy parametr:

silnia(X) -> silnia(X, 1).
silnia(X, Y) when X == 0 -> Y;
silnia(X, Y) -> silnia(X-1, X*Y).

Gdy zapisze się funkcję w ten sposób, wywołanie rekurencyjne jest ostatnią operacją wykonywaną w funkcji. Kod napisany w ten sposób działa bardzo podobnie do pętli, w związku z czym w Erlangu jest to bardzo często spotykana sytuacja. W powyższym kodzie widać też ponadto, dlaczego możliwość tworzenia funkcji o tej samej nazwie, ale różnej liczbie parametrów jest taka przydatna: można w ten sposób łatwo wprowadzać dodatkowe parametry dla potrzeb wywołań rekurencyjnych.

Snippet icon Napisz analogicznie funkcję potega(X, Y), podnoszącą X do (nieujemnej całkowitej) potęgi Y.
4.666665
Twoja ocena: Brak Ocena: 4.7 (3 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com