22.2. Aktorzy — przykład 1
W języku Java programowanie współbieżne realizowane jest przy pomocy wątków. Z wątków można jawnie korzystać również w języku Scala, ale język ten, a raczej jego biblioteka standardowa, oferuje także inne podejście — w Scali można tworzyć programy korzystające z tak zwanych aktorów. Aktora można sobie wyobrazić jako wątek ze skrzynką pocztową. Nie jest to ścisła definicja, ale na początek wystarczy. Do aktorów można wysyłać komunikaty. Implementując metodę receive aktora definiujemy jak ma on reagować na otrzymane komunikaty. Implementacja aktorów została zmieniona między wersjami 2.9 i 2.10 języka. Nowa wersja języka używa implementacji aktorów z biblioteki Akka i ta implementacja jest opisana w tym rozdziale. Należy jednak zauważyć, że biblioteka Akka zawiera bogatszą funkcjonalność w porównaniu do tego, co jest zawarte w standardowej dystrybucji języka Scala. Ten rozdział nie zastępuje dokładnego opisu biblioteki Akka.
Rozpoczniemy prezentowanie aktorów od przykładów implementujących program zawierający dwóch aktorów grających role klienta i serwera. Serwer generuje liczbę całkowitą z przedziału od 0 do 9, a zadaniem klienta jest odgadnięcie tej liczby. Klient podejmuje kolejne próby przesyłając serwerowi komunikaty będące liczbami całkowitymi. Serwer odpowiada klientowi przesyłając komunikaty będące symbolami. Jeśli przesłana serwerowi liczba jest równa liczbie wygenerowanej przez serwer, odpowiada on komunikatem 'correct i kończy działanie. W przypadku, gdy przesłana liczba jest zbyt mała, serwer odpowiada komunikatem 'tooSmall i kontynuuje pracę. Wreszcie w przypadku, gdy przesłana liczba jest zbyt duża, serwer odpowiada komunikatem 'tooBig i również kontynuuje pracę. Program jest zaimplementowany w pliku Guess1.scala. Poniższy listing pokazuje pierwszą część tego pliku.
Plik Guess1.scala: import akka.actor._
object Guess1 extends App { val system = ActorSystem("Guess1")
val server = system.actorOf(Props[GuessServer1])
val client = system.actorOf(Props(classOf[GuessClient1],server))
client ! 'startGuessing
}
W wierszu importujemy komponenty pakietu akka.actor, które są
używane w dalszej części programu. W wierszu
tworzymy tak zwany
system aktorów, pod którego kontrolą będą działać instancje aktorów. W
wierszach
i
uruchamiamy instancje obu aktorów działających w
programie — serwera i klienta. Klient jest zaimplementowany w klasie
GuessClient1, a serwer w klasie GuessServer1. Klasa klienta ma
jeden parameter. Instancji aktorów nie tworzymy samodzielnie. Zajmuje
się tym za nas system aktorów. Metodzie actorOf systemu aktorów
przekazujemy instancję klasy Props, reprezentującą przepis na
utworzenie aktora. Wartość server z wiersza
zostanie przekazana jako
argument konstruktorowi klasy GuessClient1. W wierszu
, za pomocą
metody ! przesyłamy klientowi pierwszą wiadomość.
Plik Guess1.scala: class GuessClient1(server: ActorRef) extends Actor {
println("GuessClient1: starting") private[this] var n: Int = 5
def receive = {
case 'startGuessing =>
send(n) case 'correct =>
println(s"GuessClient1: solution found: $n") context.system.shutdown() case 'tooSmall =>
println("GuessClient1: too small") n = n + 1 send(n) case 'tooBig =>
println("GuessClient1: too big") n = n - 1 send(n) } def send(msg: Int) { println(s"GuessClient1: sending $msg") server ! msg
} }
Klasa GuessClient1 rozszerza cechę Actor (wiersz ) i ma jeden
parametr — server — będący adresem aktora grającego rolę
serwera. Taki adres jest reprezentowany jako typ ActorRef. Klasa
implementuje metodę receive (wiersz
), która definiuje sposób
reakcji aktora na przychodzące do niego komunikaty. Metoda receive
powinna zwrócić funkcję częściową implementującą obsługę
komunikatów. Aktor klienta reaguje na cztery różne
komunikaty. Pierwszym z nich jest wartość 'startGuessing (wiersz
). Ten komunikat jest wysyłany tylko raz, na początku działania
programu (wiersz
) i rozpoczyna proces odgadywania liczby. W reakcji
na niego klient wysyła do serwera komunikat będący liczbą
całkowitą. Klient pamięta ostatnio próbowaną liczbę w zmiennej
prywatnej n. Odgadywanie zaczyna się od wartości 5 (wiersz
). Instrukcja z wiersza
powoduje przesłanie do serwera komunikatu
zawierającego liczbę n. Metoda ! jest wywoływana na obiekcie,
który jest adresem odbiorcy komunikatu. Parametrem tej metody jest
przesyłany komunikat. W przykładzie jest to liczba typu Int. Metoda
! działa asynchronicznie — działanie klienta nie jest blokowane. Po
wykonaniu instrukcji wysyłającej komunikat obsługa komunikatu
'startGuessing kończy się i aktor może obsłużyć następny komunikat
ze swojej skrzynki pocztowej. Jeśli w skrzynce pocztowej aktora nie ma
komunikatu do obsłużenia, działanie aktora jest wstrzymywane.
Serwer odpowiada na komunikat klienta przesyłając mu jeden z trzech
komunikatów: 'correct, 'tooSmall lub 'tooBig. W przypadku
odpowiedzi 'correct (wiersz ) klient wyświetla komunikat
zawierający odgadniętą liczbę oraz wywołuje
context.system.shutdown(), żeby zakończyć działanie całego systemu
aktorów. Wartość context jest odwołaniem do kontekstu aktora i jest
typu ActorContext. Metoda system kontekstu zwraca system aktorów,
a metoda shutdown zatrzymuje go. W przypadku odpowiedzi 'tooSmall
(wiersz
) lub 'tooBig (wiersz
) klient zmienia wartość zmiennej
n o wartość 1 w górę lub w dół i przesyła do serwera kolejny
komunikat z nową wartością.
Klasa GuessServer1 jest implementacją aktora grającego rolę serwera.
Plik Guess1.scala: class GuessServer1 extends Actor {
val n = scala.util.Random.nextInt(10) println("GuessServer1: starting") def receive = {
case x:Int if x > n =>
send(sender, 'tooBig) case x:Int if x < n =>
send(sender, 'tooSmall) case x:Int =>
send(sender, 'correct) } def send(sender: ActorRef, msg: Symbol) { println(s"GuessServer1: sending $msg") sender ! msg } }
Ta klasa również rozszerza cechę Actor (wiersz ) oraz implementuje
metodę receive (wiersz
). Konstruktor inicjalizuje wartość n,
czyli liczbę którą należy odgadnąć. Funkcja częściowa zwracana przez
metodę receive obsługuje komunikaty w postaci liczby całkowitej, ale
dozory wydzielają z nich trzy przypadki. Jeśli otrzymana liczba jest
równa n (wiersz
), serwer odsyła nadawcy komunikat
'correct. Jeśli otrzymana liczba jest większa (wiersz
) lub
mniejsza (wiersz
) od n, serwer odsyła komunikat 'tooBig lub
'tooSmall. W każdym z tych przypadków wiadomość odsyłana jest do
nadawcy otrzymanej wiadomości dzięki wykorzystaniu metody sender,
zwracającej adres nadawcy.
Poniżej przedstawiony jest przykładowy rezultat wykonania programu.
$ scala Guess1 GuessClient1: starting GuessServer1: starting GuessClient1: sending 5 GuessServer1: sending 'tooBig GuessClient1: too big GuessClient1: sending 4 GuessServer1: sending 'tooBig GuessClient1: too big GuessClient1: sending 3 GuessServer1: sending 'tooBig GuessClient1: too big GuessClient1: sending 2 GuessServer1: sending 'correct GuessClient1: solution found: 2