Scala

Futureをマスターする -はじめに-

More than 3 years have passed since last update.

Futureについてちゃんと使いこなせるようになろうと思い立ち全操作ひと通りやってみることにした。

※Scala 2.11.1のscaladocの全操作となります。

trait Awaitable: 5

trait Future: 16

object Future: 10


シリーズ目次

はじめに

コンパニオンオブジェクト操作

インスタンス操作

おまけ

Futureにハマる New!


はじめに

全操作やっていく前に、最低限Futureの動きやFutureと組み合わせてよく使うものをはじめに確認しておく。

なお、以下の記事とても勉強になりましたので併せて読むと良いかも。リンクさせて頂きます。


その1

import scala.concurrent._

import ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration

object Main extends App {
val msg = "hello"
val f: Future[String] = Future {
Thread.sleep(1000)
msg * 5
}

println(f.isCompleted) // false
val result1: Option[Try[String]] = f.value
println(result1) // None

Thread.sleep(2000)

println(f.isCompleted) // true
val result2: Option[Try[String]] = f.value
println(result2.get.get) // hellohellohellohellohello
}

Future {}はFuture#applyであり、Futureインスタンスを生成する際の典型的なコードである。

なお、ネットでよく見るサンプルも公式ドキュメントもfuture{...}で生成しているものばかりなのですが、Scala2.11からscala.concurrentのfutureメソッドはdeprecated扱いになっており、代わりにFutureを使うよう記述されています。

よって本記事ではfutre{ ... }は使用せずFuture{ ... }で統一しています。

これで、インスタンス生成と同時にMainとは別に並列実行が行われる。

isCompleteで並列実行側が終了したかどうかを確認できる。

valueで現時点の値を取得できる。終わっていなければNone、終わっていればSome[Try[T]]となる。

なお、このisCompletedやvalueはFutureではなく、mix-inされているAwaitableトレイトで宣言されたメソッドである。

Awaitableトレイトにはその他のメソッドとしてresult、ready、onCompleteがあるのだが、それらのメソッドを実行してみると、Don't call `Awaitable` methods directly, use the `Await` object.とこちらはコンパイルエラーになり強制的に呼び出せないようになっている。


その2

import scala.concurrent._

import ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration

object Main extends App {
val msg = "hello"
val f: Future[String] = Future {
Thread.sleep(1000)
msg * 5
}

val result: String = Await.result(f, Duration.Inf)
println(result) // hellohellohellohellohello
}

その1でコンパイルエラーメッセージにあったAwaitを使ったプログラム例である。

Main側でなんらかFutureの処理を待っておかないとMainスレッドが終了してしまうため、その1ではThread.sleepで待っていたわけだが当然こんなやり方は通常はせず、Awaitを使うことで特定のFutureインスタンスの処理を待つことができるようになる。

なお、Await#resultの問題点はFuture内で例外が起きるとこのメソッド実行時にも例外で返ってくる点であり、せっかくvalueの戻り値がTryになっているのにこの旨みを活かせてないように感じる。

こういうときはAwait#readyを使用する。Await#resultでは処理終了のタイミング調整&値取り出しの2つを行っているのに対し、readyは処理終了のタイミング調整だけを行う。よって値取り出しコードは自分で記述する。

Await.ready(f, Duration.Inf)

f.value.get match {
case Success(msg) => println(msg)
case Failure(ex) => println(ex.getMessage)
}


その3

import scala.concurrent._

import ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.util.{ Try, Success, Failure }

object Main extends App {
val msg = "hello"
val f: Future[String] = Future {
Thread.sleep(1000)
msg * 2
}
f.onSuccess { case msg: String => println(msg) }
f.onFailure { case t: Throwable => println(t.getMessage()) }

Await.ready(f, Duration.Inf)
}

コールバック関数を登録するコード例。

成功時と失敗時の両方を記述するなら通常はonCompleteを使う。

f.onComplete {

case Success(msg) => println(msg)
case Failure(t) => println(t.getMessage())
}

※なおこれら終了時の処理を記述するonXxxコールバック関数はExecutionContextを必要とする(implicitに)。つまり別スレッドでの実行となる。

その3の例だと、Mainスレッド、Future.apply内部を実行するスレッド、Future#onXxxの終了時処理を実行するスレッドの計3スレッド(Future#applyおよび終了処理の実行はスレッドプール内の同じスレッドが充てられるかも、だが)が登場することとなる。

終了時の処理がMainスレッドではなく別スレッドで実行されるということを忘れていると、 Futureにハマる のような事になるので注意が必要。

少しフライングして言うと、map/flatMapなどFutureの値に対する操作処理は大抵ExecutionContextを必要とし、Mainとは別のスレッドで実行される(ことはもちろんのこと、for式等でflatMapやmapをチェーンさせた場合も、チェーンしたflatMapとmapに同じスレッドが充てられる保証はない。ExecutionContext.globalを使用している場合に内部でForkJoinPoolが採用されていると同じスレッドが充てられやすくはなりそう)ものがほとんどだ。


メソッドコンプリート状況

今回でカバーしたメソッド数は以下。

trait Awaitable: 5/5

trait Future: 3/16

object Future: 1/10