Aktorzy scali: odbieraj a reaguj


110

Pozwolę sobie najpierw powiedzieć, że mam całkiem spore doświadczenie w Javie, ale dopiero niedawno zainteresowałem się językami funkcjonalnymi. Niedawno zacząłem patrzeć na Scalę, która wydaje się bardzo fajnym językiem.

Jednak czytałem o strukturze aktora Scali w programowaniu w Scali i jest jedna rzecz, której nie rozumiem. W rozdziale 30.4 jest napisane, że używanie reactzamiast receiveumożliwia ponowne użycie wątków, co jest dobre dla wydajności, ponieważ wątki są drogie w JVM.

Czy to oznacza, że ​​jeśli pamiętam, żeby zadzwonić reactzamiast dzwonić receive, mogę założyć tylu aktorów, ilu zechcę? Zanim odkryłem Scalę, bawiłem się z Erlangiem, a autor Programming Erlang chwali się, że bez wysiłku stworzył ponad 200 000 procesów. Nie chciałbym tego robić z wątkami Java. Na jakie ograniczenia patrzę w Scali w porównaniu z Erlangiem (i Javą)?

Jak działa ponowne użycie tego wątku w Scali? Załóżmy dla uproszczenia, że ​​mam tylko jeden wątek. Czy wszyscy aktorzy, których zacznę uruchamiać sekwencyjnie w tym wątku, czy będzie miało miejsce przełączanie zadań? Na przykład, jeśli uruchomię dwóch aktorów, którzy wysyłają sobie wiadomości ping-pong, czy zaryzykuję zakleszczenie, jeśli zaczną się w tym samym wątku?

Według Programming in Scala pisanie aktorów do użycia reactjest trudniejsze niż z receive. Brzmi to wiarygodnie, ponieważ reactnie wraca. Jednak książka pokazuje, jak można umieścić reactwewnątrz pętli za pomocą Actor.loop. W rezultacie otrzymujesz

loop {
    react {
        ...
    }
}

który wydaje mi się dość podobny do

while (true) {
    receive {
        ...
    }
}

który został użyty wcześniej w książce. Mimo to książka mówi, że „w praktyce programy będą wymagały co najmniej kilku receive”. Więc czego tu brakuje? Co może receivezrobić, reactczego nie może poza powrotem? I dlaczego mnie to obchodzi?

Na koniec dochodzę do sedna tego, czego nie rozumiem: książka ciągle wspomina, jak używanie reactumożliwia odrzucenie stosu wywołań w celu ponownego użycia wątku. Jak to działa? Dlaczego konieczne jest odrzucenie stosu wywołań? I dlaczego można odrzucić stos wywołań, gdy funkcja kończy się przez zgłoszenie wyjątku ( react), ale nie wtedy, gdy kończy się zwracaniem ( receive)?

Mam wrażenie, że programowanie w Scali tuszowało tu kilka kluczowych kwestii, a szkoda, bo poza tym to naprawdę świetna książka.


Odpowiedzi:


78

Po pierwsze, każdy czekający aktor receivezajmuje wątek. Jeśli nic nie otrzyma, nic nie zrobi. Aktor reactnie zajmuje żadnego wątku, dopóki czegoś nie otrzyma. Gdy coś otrzyma, przydzielany jest do niego wątek i jest w nim inicjowany.

Teraz część inicjująca jest ważna. Oczekuje się, że wątek odbierający coś zwróci, a wątek reagujący nie. Zatem poprzedni stan stosu na końcu ostatniego reactmoże być i jest całkowicie odrzucony. Brak konieczności zapisywania ani przywracania stanu stosu przyspiesza uruchamianie wątku.

Istnieje wiele powodów, dla których możesz chcieć jednego lub drugiego. Jak wiesz, posiadanie zbyt wielu wątków w Javie nie jest dobrym pomysłem. Z drugiej strony, ponieważ trzeba wcześniej dołączyć aktora do wątku react, jest to szybsze do receiveprzesłania niż reactdo niego. Więc jeśli masz aktorów, którzy otrzymują wiele wiadomości, ale robią z nimi bardzo mało, dodatkowe opóźnienie reactmoże spowodować, że będzie to zbyt wolne dla twoich celów.


21

Odpowiedź brzmi „tak” - jeśli twoi aktorzy nie blokują niczego w twoim kodzie i używasz react, możesz uruchomić swój program „współbieżny” w ramach jednego wątku (spróbuj ustawić właściwość systemową, actors.maxPoolSizeaby się dowiedzieć).

Jednym z bardziej oczywistych powodów, dla których konieczne jest odrzucenie stosu wywołań, jest to, że w przeciwnym razie loopmetoda zakończyłaby się rozszerzeniem StackOverflowError. W rzeczywistości framework dość sprytnie kończy a react, rzucając a SuspendActorException, który jest przechwytywany przez kod pętli, który następnie uruchamia reactponownie za pomocą andThenmetody.

Spójrz na mkBodymetodę in, Actora następnie seqmetodę, aby zobaczyć, jak pętla sama się zmienia - okropnie sprytna rzecz!


20

Te stwierdzenia o „odrzuceniu stosu” również przez chwilę zdezorientowały mnie i myślę, że teraz rozumiem i to jest teraz moje zrozumienie. W przypadku "odbioru" istnieje dedykowane blokowanie wątków w wiadomości (za pomocą object.wait () na monitorze), co oznacza, że ​​cały stos wątków jest dostępny i gotowy do kontynuowania od momentu "oczekiwania" na odebranie wiadomość. Na przykład, jeśli masz następujący kod

  def a = 10;
  while (! done)  {
     receive {
        case msg =>  println("MESSAGE RECEIVED: " + msg)
     }
     println("after receive and printing a " + a)
  }

wątek czekałby w wywołaniu odbierającym, aż wiadomość zostanie odebrana, a następnie kontynuowałby i drukował komunikat „po odebraniu i wydrukowaniu 10” z wartością „10”, która znajduje się w ramce stosu przed zablokowaniem wątku.

W przypadku, gdy nie ma takiego dedykowanego wątku, cała treść metody reago jest przechwytywana jako zamknięcie i jest wykonywana przez dowolny dowolny wątek na odpowiednim aktorze otrzymującym wiadomość. Oznacza to, że zostaną wykonane tylko te instrukcje, które mogą być przechwycone jako samo zamknięcie, i wtedy pojawia się zwracany typ „Nic”. Rozważmy następujący kod

  def a = 10;
  while (! done)  {
     react {
        case msg =>  println("MESSAGE RECEIVED: " + msg)
     }
     println("after react and printing a " + a) 
  }

Gdyby reakcja miała zwracany typ void, oznaczałoby to, że dozwolone jest posiadanie instrukcji po wywołaniu „reakcja” (w przykładzie instrukcja println, która drukuje wiadomość „po zareaguj i wypisz 10”), ale w rzeczywistości nigdy nie zostałby wykonany, ponieważ tylko treść metody „reaguj” jest przechwytywana i sekwencjonowana do późniejszego wykonania (po nadejściu wiadomości). Ponieważ kontrakt reaguje na zwracany typ „Nothing”, nie może być żadnych instrukcji po reakcji, a nie ma powodu, aby utrzymywać stos. W powyższym przykładzie zmienna „a” nie musiałaby być utrzymywana, ponieważ instrukcje po wywołaniach reakcji nie są w ogóle wykonywane. Zauważ, że wszystkie potrzebne zmienne przez ciało reagują już są przechwycone jako zamknięcie, więc może działać dobrze.

Platforma aktora java Kilim w rzeczywistości zajmuje się obsługą stosu, zapisując stos, który jest rozwijany w odpowiedzi na otrzymanie wiadomości.


Dzięki, to było bardzo pouczające. Ale czy nie miałeś na myśli +afragmentów kodu zamiast +10?
jqno,

Świetna odpowiedź. Tego też nie rozumiem.
santiagobasulto


0

Nie wykonałem żadnej większej pracy ze scala / akka, ale rozumiem, że istnieje bardzo znacząca różnica w sposobie planowania aktorów. Akka to po prostu sprytna pula wątków, która dzieli wykonanie aktorów w czasie ... Każdy wycinek czasu będzie wykonywał jedną wiadomość do zakończenia przez aktora, inaczej niż w Erlangu, który mógłby być wykonywany na instrukcję ?!

To prowadzi mnie do wniosku, że reakcja jest lepsza, ponieważ sugeruje bieżącemu wątkowi, aby wziął pod uwagę innych aktorów do planowania, w których jako odbieranie „może” zaangażować bieżący wątek do kontynuowania wykonywania innych komunikatów dla tego samego aktora.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.