Od jakiegoś czasu obserwuję rosnącą widoczność funkcjonalnych języków programowania i funkcji. Zajrzałem do nich i nie widziałem powodu odwołania.
Niedawno byłem na prezentacji Kevina Smitha „Basics of Erlang” w Codemash .
Podobała mi się prezentacja i dowiedziałem się, że wiele atrybutów programowania funkcjonalnego znacznie ułatwia unikanie problemów z wątkami / współbieżnością. Rozumiem, że brak stanu i zmienności uniemożliwia wielu wątkom zmianę tych samych danych, ale Kevin powiedział (jeśli dobrze zrozumiałem) cała komunikacja odbywa się za pośrednictwem wiadomości, a wiadomości są przetwarzane synchronicznie (ponownie unikając problemów z współbieżnością).
Ale przeczytałem, że Erlang jest używany w wysoce skalowalnych aplikacjach (przez cały powód, dla którego Ericsson go stworzył). Jak efektywnie obsługiwać tysiące żądań na sekundę, jeśli wszystko jest obsługiwane jako synchronicznie przetwarzana wiadomość? Czy to nie jest powód, dla którego zaczęliśmy przechodzić w kierunku przetwarzania asynchronicznego - abyśmy mogli skorzystać z wielu wątków operacji w tym samym czasie i osiągnąć skalowalność? Wygląda na to, że ta architektura, choć bezpieczniejsza, jest krokiem wstecz pod względem skalowalności. czego mi brakuje?
Rozumiem, że twórcy Erlanga celowo unikali obsługi wątków, aby uniknąć problemów ze współbieżnością, ale uważałem, że wielowątkowość jest niezbędna do osiągnięcia skalowalności.
W jaki sposób funkcjonalne języki programowania mogą być z natury bezpieczne dla wątków, a jednocześnie nadal skalować?
źródło
Odpowiedzi:
Język funkcjonalny nie polega (na ogół) na mutowaniu zmiennej. Z tego powodu nie musimy chronić „stanu wspólnego” zmiennej, ponieważ wartość jest stała. To z kolei pozwala uniknąć większości przeskoków, przez które muszą przejść tradycyjne języki, aby zaimplementować algorytm między procesorami lub maszynami.
Erlang idzie dalej niż tradycyjne języki funkcjonalne, wypiekając w systemie przekazywania komunikatów, który pozwala wszystkim działać w systemie opartym na zdarzeniach, w którym fragment kodu martwi się tylko o odbieranie wiadomości i wysyłanie wiadomości, nie martwiąc się o większy obraz.
Oznacza to, że programista (nominalnie) nie przejmuje się tym, że wiadomość zostanie obsłużona na innym procesorze lub maszynie: po prostu wysłanie wiadomości jest wystarczające, aby kontynuować. Jeśli zależy mu na odpowiedzi, będzie na nią czekał jako kolejna wiadomość .
Końcowym rezultatem tego jest to, że każdy fragment jest niezależny od każdego innego fragmentu. Brak współdzielonego kodu, brak współdzielonego stanu i wszystkie interakcje pochodzące z systemu wiadomości, który może być dystrybuowany na wiele elementów sprzętu (lub nie).
Porównaj to z tradycyjnym systemem: musimy umieścić muteksy i semafory wokół „chronionych” zmiennych i wykonywania kodu. Mamy ścisłe powiązanie w wywołaniu funkcji przez stos (oczekiwanie na powrót). Wszystko to tworzy wąskie gardła, które są mniejszym problemem we wspólnym systemie niczego, takim jak Erlang.
EDYCJA: Powinienem również zaznaczyć, że Erlang jest asynchroniczny. Wysyłasz wiadomość i być może / kiedyś wróci kolejna wiadomość. Albo nie.
Ważna jest również uwaga Spencera dotycząca wykonania poza kolejnością.
źródło
System kolejkowania wiadomości jest fajny, ponieważ skutecznie daje efekt „odpal i poczekaj na wynik”, czyli synchroniczną część, o której czytasz. To, co czyni to niesamowicie niesamowitym, to fakt, że oznacza to, że linie nie muszą być wykonywane sekwencyjnie. Rozważ następujący kod:
r = methodWithALotOfDiskProcessing(); x = r + 1; y = methodWithALotOfNetworkProcessing(); w = x * y
Rozważ przez chwilę, że wykonanie metody methodWithALotOfDiskProcessing () zajmuje około 2 sekund, a wykonanie metody methodWithALotOfNetworkProcessing () zajmuje około 1 sekundy. W języku proceduralnym wykonanie tego kodu zajęłoby około 3 sekund, ponieważ wiersze byłyby wykonywane sekwencyjnie. Marnujemy czas, czekając na ukończenie jednej metody, która mogłaby działać jednocześnie z drugą bez konkurowania o jeden zasób. W funkcjonalnym języku linie kodu nie narzucają, kiedy procesor będzie je próbował. Język funkcjonalny mógłby spróbować czegoś takiego:
Execute line 1 ... wait. Execute line 2 ... wait for r value. Execute line 3 ... wait. Execute line 4 ... wait for x and y value. Line 3 returned ... y value set, message line 4. Line 1 returned ... r value set, message line 2. Line 2 returned ... x value set, message line 4. Line 4 returned ... done.
Jakie to jest świetne? Kontynuując kod i czekając tylko tam, gdzie jest to konieczne, automagicznie zredukowaliśmy czas oczekiwania do dwóch sekund! : D Więc tak, chociaż kod jest synchroniczny, ma zwykle inne znaczenie niż w językach proceduralnych.
EDYTOWAĆ:
Gdy zrozumiesz tę koncepcję w połączeniu z postem Godeke, łatwo wyobrazić sobie, jak łatwo jest skorzystać z wielu procesorów, farm serwerów, nadmiarowych magazynów danych i kto wie, co jeszcze.
źródło
Jest prawdopodobne, że mieszasz synchronicznie z sekwencyjnym .
Treść funkcji w erlang jest przetwarzana sekwencyjnie. Więc to, co Spencer powiedział o tym „automagicznym efekcie”, nie odnosi się do erlanga. Możesz jednak modelować to zachowanie za pomocą erlang.
Na przykład możesz stworzyć proces, który oblicza liczbę słów w linii. Ponieważ mamy kilka wierszy, tworzymy jeden taki proces dla każdego wiersza i otrzymujemy odpowiedzi, aby obliczyć z niego sumę.
W ten sposób tworzymy procesy, które wykonują „ciężkie” obliczenia (wykorzystując dodatkowe rdzenie, jeśli są dostępne), a później zbieramy wyniki.
-module(countwords). -export([count_words_in_lines/1]). count_words_in_lines(Lines) -> % For each line in lines run spawn_summarizer with the process id (pid) % and a line to work on as arguments. % This is a list comprehension and spawn_summarizer will return the pid % of the process that was created. So the variable Pids will hold a list % of process ids. Pids = [spawn_summarizer(self(), Line) || Line <- Lines], % For each pid receive the answer. This will happen in the same order in % which the processes were created, because we saved [pid1, pid2, ...] in % the variable Pids and now we consume this list. Results = [receive_result(Pid) || Pid <- Pids], % Sum up the results. WordCount = lists:sum(Results), io:format("We've got ~p words, Sir!~n", [WordCount]). spawn_summarizer(S, Line) -> % Create a anonymous function and save it in the variable F. F = fun() -> % Split line into words. ListOfWords = string:tokens(Line, " "), Length = length(ListOfWords), io:format("process ~p calculated ~p words~n", [self(), Length]), % Send a tuple containing our pid and Length to S. S ! {self(), Length} end, % There is no return in erlang, instead the last value in a function is % returned implicitly. % Spawn the anonymous function and return the pid of the new process. spawn(F). % The Variable Pid gets bound in the function head. % In erlang, you can only assign to a variable once. receive_result(Pid) -> receive % Pattern-matching: the block behind "->" will execute only if we receive % a tuple that matches the one below. The variable Pid is already bound, % so we are waiting here for the answer of a specific process. % N is unbound so we accept any value. {Pid, N} -> io:format("Received \"~p\" from process ~p~n", [N, Pid]), N end.
A tak to wygląda, gdy uruchomimy to w powłoce:
Eshell V5.6.5 (abort with ^G) 1> Lines = ["This is a string of text", "and this is another", "and yet another", "it's getting boring now"]. ["This is a string of text","and this is another", "and yet another","it's getting boring now"] 2> c(countwords). {ok,countwords} 3> countwords:count_words_in_lines(Lines). process <0.39.0> calculated 6 words process <0.40.0> calculated 4 words process <0.41.0> calculated 3 words process <0.42.0> calculated 4 words Received "6" from process <0.39.0> Received "4" from process <0.40.0> Received "3" from process <0.41.0> Received "4" from process <0.42.0> We've got 17 words, Sir! ok 4>
źródło
Kluczową rzeczą, która umożliwia Erlangowi skalowanie, jest współbieżność.
System operacyjny zapewnia współbieżność za pomocą dwóch mechanizmów:
Procesy nie mają wspólnego stanu - jeden proces nie może ulec awarii z powodu innego projektu.
Wątki udostępniają stan - jeden wątek może ulec awarii zgodnie z projektem - to twój problem.
Z Erlangiem - jeden proces systemu operacyjnego jest używany przez maszynę wirtualną, a maszyna wirtualna zapewnia współbieżność programowi Erlang nie za pomocą wątków systemu operacyjnego, ale przez udostępnianie procesów Erlang - to znaczy, że Erlang implementuje własny licznik czasu.
Te procesy Erlang komunikują się ze sobą, wysyłając komunikaty (obsługiwane przez maszynę wirtualną Erlang, a nie system operacyjny). Procesy Erlang zwracają się do siebie za pomocą identyfikatora procesu (PID), który ma trzyczęściowy adres
<<N3.N2.N1>>
:Dwa procesy na tej samej maszynie wirtualnej, na różnych maszynach wirtualnych na tej samej maszynie lub na dwóch maszynach komunikują się w ten sam sposób - dlatego skalowanie jest niezależne od liczby maszyn fizycznych, na których wdrażasz aplikację (w pierwszym przybliżeniu).
Erlang jest bezpieczny dla wątków tylko w trywialnym sensie - nie ma nici. (Język, to znaczy SMP / wielordzeniowa maszyna wirtualna używa jednego wątku systemu operacyjnego na rdzeń).
źródło
Możesz nie rozumieć, jak działa Erlang. Środowisko wykonawcze Erlang minimalizuje przełączanie kontekstu na procesorze, ale jeśli dostępnych jest wiele procesorów, wszystkie są używane do przetwarzania komunikatów. Nie masz „wątków” w takim sensie, jak w innych językach, ale jednocześnie możesz przetwarzać wiele wiadomości.
źródło
Wiadomości Erlang są czysto asynchroniczne, jeśli chcesz otrzymać synchroniczną odpowiedź na swoją wiadomość, musisz jawnie zakodować. Prawdopodobnie powiedziano, że komunikaty w oknie komunikatu o procesie są przetwarzane sekwencyjnie. Każda wiadomość wysłana do procesu trafia do tego okna komunikatu procesu, a proces może wybrać jedną wiadomość z tego pola, przetworzyć ją, a następnie przejść do następnej, w kolejności, którą uważa za stosowną. Jest to bardzo sekwencyjna czynność i właśnie to robi blok odbioru.
Wygląda na to, że pomyliłeś synchroniczne i sekwencyjne, jak wspomniał Chris.
źródło
Przejrzystość referencyjna: patrz http://en.wikipedia.org/wiki/Referential_transparency_(computer_science)
źródło
W języku czysto funkcjonalnym kolejność obliczeń nie ma znaczenia - w aplikacji funkcji fn (arg1, .. argn) n argumentów może być ocenianych równolegle. Gwarantuje to wysoki poziom (automatycznego) równoległości.
Erlang używa modelu procesu, w którym proces może działać na tej samej maszynie wirtualnej lub na innym procesorze - nie ma sposobu, aby to stwierdzić. Jest to możliwe tylko dlatego, że wiadomości są kopiowane między procesami, nie ma stanu współdzielonego (zmiennego). Równoległość wieloprocesorowa sięga znacznie dalej niż wielowątkowość, ponieważ wątki zależą od pamięci współdzielonej, może to być tylko 8 wątków działających równolegle na 8-rdzeniowym procesorze, podczas gdy przetwarzanie wieloprocesowe może być skalowane do tysięcy równoległych procesów.
źródło