22.1. Obliczenia asynchroniczne
Scala udostępnia w pakiecie scala.concurrent szereg narzędzi służących do programowania współbieżnego. Jednym z tych narzędzi jest Future, reprezentujący asynchronicznie wykonujące się obliczenia, mogące dostarczyć wynik w przyszłości. Plik FibFuture.scala zawiera przykład wykorzystania Future.
Plik FibFuture.scala: def fib(n: Int): Int = n match { case 0 | 1 => 1 case n if n < 0 => throw new IllegalArgumentException(s"$n < 0") case _ => fib(n-1) + fib(n-2) } def fibFuture(n: Int) { import scala.concurrent._
import ExecutionContext.Implicits.global
println(s"fib($n) calculation")
val f: Future[Int] = Future { fib(n) }
println("calculating...")
f.onSuccess { case result => println(s"success: $result") }
f.onFailure { case result => println(s"failure: $result") }
f.onComplete { case result => println(s"completed: $result") }
println("the results will be shown when ready")
}
Metoda fib służy do obliczania liczby Fibonacciego. Użyty został celowo nieefektywny czasowo algorytm, aby łatwo symulować czasochłonne operacje. Dodatkowo, metoda wyrzuca wyjątek w przypadku podania jako argumentu liczby ujemnej.
scala> :load FibFuture.scala Loading FibFuture.scala... fib: (n: Int)Int fibFuture: (n: Int)Unit scala> fib(5) res0: Int = 8
scala> fib(-1) java.lang.IllegalArgumentException: -1 < 0 at .fib(<console>:12) ... 33 elided
Metoda fibFuture demonstruje wykorzystanie Future. Ciało metody
rozpoczyna się od zaimportowania składowych pakietu scala.concurrent
(wiersz ) oraz niejawnie przekazywanej wartości implementującej w
domyślny sposób tak zwany kontekst wykonania (wiersz
). Obecność
takiego kontekstu jest wymagana przez niektóre metody działające na
Future. W wierszu
wywoływana jest metoda apply obiektu Future,
której argumentem jest wywołanie metody fib. Metoda Future.apply
uruchamia przekazany jej blok kodu, ale robi to asynchronicznie, co
objawia się tym, że jej działanie kończy się bez oczekiwania na
zakończenie obliczeń przez uruchomiony blok kodu. W konsekwencji,
wynikiem metody nie może być wynik ewaluacji tego bloku. Zamiast tego,
wynikiem metody jest instancja cechy Future sparametryzowanej typem
przekazanego metodzie future bloku kodu, którym w naszym przypadku
jest Int. Wartość zwracana przez metodę future w wierszu
ma typ
Future[Int]. Na wartości Future można zarejestrować funkcje, które
mają zostać wykonane po zakończeniu obliczeń. W metodzie fibFuture
służą do tego wywołania metod onSuccess, onFailure i onComplete
z wierszy
,
i
. Przed dalszymi wyjaśnienianiami sprawdźmy
działanie metody z argumentem równym 40.
scala> fibFuture(40)fib(40) calculation
calculating...
the results will be shown when ready
scala> success: 165580141
completed: Success(165580141)
W wierszu następuje wywołanie metody fibFuture z konsoli. Wiersze
,
i
pokazują komunikaty wyświetlone przez metody println z
wierszy
,
i
. Po wyświetleniu tych komunikatów metoda kończy
działanie, a konsola wyświetla łańcuch zachęty (wiersz
). Choć metoda
fibFuture zakończyła działanie, równolegle trwają obliczenia z bloku
kodu przekazanego metodzie future w wierszu
. Po ich zakończeniu
może rozpocząć się wykonanie funkcji zarejestrowanych na obiekcie f
w wierszach
,
i
. Metoda onSuccess rejestruje funkcję, która ma
zostać wykonana w przypadku pomyślnego wykonania obliczeń z obiektu
Future. W rozważanym przypadku wywołanie kończy się pomyślnie i kod
funkcji przekazanej do metody onSuccess w wierszu
zostaje wykonany
z rezultatem obliczeń jako argumentem, co powoduje wyświetlenie
komunikatu z wiersza
. Metoda onFailure rejestruje funkcję, która
ma zostać wykonana w przypadku niepomyślnego zakończenia obliczeń, a
więc gdy te obliczenia zostają przerwane wyrzuceniem
wyjątku. Argumentem przekazanym funkcji staje się ten wyjątek. W
rozważanym przypadku obliczenia nie kończą się wyjątkiem i w
rezultacie funkcja przekazana metodzie onFailure w wierszu
nie
zostaje wywołana. Metoda onComplete rejestruje funkcję, która ma
zostać wykonana po zakończeniu obliczeń niezależnie od tego, jak się
zakończyły. Argumentem przekazanym funkcji jest instancja klasy
Try. W przypadku pomyślnego obliczenia wyniku przez Future
funkcja otrzymuje ten wynik opakowany w instancję klasy Success. W
przeciwnym przypadku otrzymuje wyjątek opakowany instancją klasy
Failure. Ponieważ wywołanie metody fibFuture skończyło się
pomyślnie, funkcja zarejestrowana w wierszu
przez metodę
onComplete otrzymała wynik obliczeń opakowany w instancję
Success. Poniższy przykład pokazuje wyniki wywołania metody
fibFuture z argumentem ujemnym. W takim przypadku wykonana zostanie
funkcja zarejestrowana przez metodę onFailure, a funkcja
zarejestowana przez metodę onComplete otrzymuje jako argument
wyjątek opakowany w instancję klasy Failure.
scala> fibFuture(-1) fib(-1) calculation calculating... the results will be shown when ready
scala> completed: Failure(java.lang.IllegalArgumentException: -1 < 0) failure: java.lang.IllegalArgumentException: -1 < 0
Istnieje możliwość zablokowania działania kodu w oczekiwaniu na zakończenie obliczeń dokonywanych przez Future za pomocą metody result obiektu Await. Oprócz obliczanego obiektu Future metoda ma drugi parametr, typu Duration, reprezentujący maksymalny czas oczekiwania na zakończenie obliczeń. Duration jest zdefiniowany w pakiecie scala.concurrent.duration. Zaimportowanie składowych tego pakietu pozwala tworzyć obiekty Duration za pomocą wywoływanych na liczbach metod reprezentujących jednostki czasu, jak w poniższych przykładach:
scala> import scala.concurrent.duration._ import scala.concurrent.duration._ scala> 2.minutes res4: scala.concurrent.duration.FiniteDuration = 2 minutes scala> (1.2).seconds res5: scala.concurrent.duration.FiniteDuration = 1200 milliseconds scala> 1.hour res6: scala.concurrent.duration.FiniteDuration = 1 hour
Metoda result czeka na zakończenie obliczeń, a w przypadku ich pomyślnego zakończenia, zwraca wynik.
scala> import scala.concurrent._, ExecutionContext.Implicits.global import scala.concurrent._ import ExecutionContext.Implicits.global scala> Await.result(Future{ fib(40) }, 1.minute) res7: Int = 165580141
Jeśli obliczenia trwają zbyt długo, oczekiwanie zostaje przerwane i zostaje wyrzucony wyjątek.
scala> Await.result(Future{ fib(40) }, 1.second) java.util.concurrent.TimeoutException: Futures timed out after [1 second] at scala.concurrent.impl.Promise$DefaultPromise.ready(Promise.scala:219) at scala.concurrent.impl.Promise$DefaultPromise.result(Promise.scala:223) at scala.concurrent.Await$$anonfun$result$1.apply(package.scala:190) at scala.concurrent.BlockContext$DefaultBlockContext$.blockOn(BlockContext.scala:53) at scala.concurrent.Await$.result(package.scala:190) ... 33 elided
Wyjątek zostanie wyrzucony również wtedy, kiedy obliczanie Future się nim zakończy.
scala> Await.result(Future{ fib(-7) }, 1.day) java.lang.IllegalArgumentException: -7 < 0 at .fib(<console>:12) at $anonfun$1.apply$mcI$sp(<console>:19) at $anonfun$1.apply(<console>:19) at $anonfun$1.apply(<console>:19) at scala.concurrent.impl.Future$PromiseCompletingRunnable.liftedTree1$1(Future.scala:24) at scala.concurrent.impl.Future$PromiseCompletingRunnable.run(Future.scala:24) at scala.concurrent.impl.ExecutionContextImpl$AdaptedForkJoinTask.exec(ExecutionContextImpl.scala:121) at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260) at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339) at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979) at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)