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)

Język programowania Scala Wydanie 2. Copyright © Grzegorz Balcerek 2016

Licencja Creative Commons

Ten utwór jest dostępny na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowe.

Oracle and Java are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners.