1. 概要

データベースクエリや別のHTTPサービスの呼び出しなどの一部の操作は、完了するまでに時間がかかる場合があります。 それらをメインスレッドで実行すると、それ以上のプログラム実行がブロックされ、パフォーマンスが低下します。

このチュートリアルでは、 Future に焦点を当てます。これは、バックグラウンドで操作を実行するためのScalaアプローチであり、この問題の解決策です。

2. 将来

Future は、非同期計算の結果を表しますが、まだ使用できる場合とできない場合があります。

新しいFutureを作成すると、Scalaは新しいスレッドを生成し、そのコードを実行します。 実行が終了すると、計算結果(値または例外)がFutureに割り当てられます。

3. Futureを作成します

3.1. ExecutionContext

作成する前に 未来暗黙的を提供する必要があります ExecutionContext。 これは、どのスレッドプールでどのように 未来 コードが実行されます。 ExecutorまたはExecutorServiceから作成できます。

val forkJoinPool: ExecutorService = new ForkJoinPool(4)
implicit val forkJoinExecutionContext: ExecutionContext = 
  ExecutionContext.fromExecutorService(forkJoinPool)

val singleThread: Executor = Executors.newSingleThreadExecutor()
implicit val singleThreadExecutionContext: ExecutionContext = 
  ExecutionContext.fromExecutor(singleThread)

グローバル組み込みExecutionContextもあります。これは、並列処理レベルが使用可能なプロセッサの数に設定されたForkJoinPoolを使用します。

implicit val globalExecutionContext: ExecutionContext = ExecutionContext.global

次のセクションでは、 ExecutionContext.global を使用して、単一のインポートを使用して使用できるようにします。

import scala.concurrent.ExecutionContext.Implicits.global

3.2. スケジュール将来

ExecutionContext ができたので、バックグラウンドで長時間実行される操作を実行するFutureを作成します。 これをThread.sleepでシミュレートします。

def generateMagicNumber(): Int = {
  Thread.sleep(3000L)
  23
}
val generatedMagicNumberF: Future[Int] = Future {
  generateMagicNumber()
}

電話するとき Future.apply への呼び出しで generateMagicNumber 内部では、 未来ランタイムは別のスレッドでそれを実行しますメソッドを呼び出した結果をに渡したように見えるかもしれません未来 、 でも、 Future.apply としてそれを取る名前でパラメータ。 暗黙のExecutionContextによって提供されるスレッドに評価を移動します。

3.3. 計算された将来

すでに計算された値がある場合、Futureを取得するために非同期計算を開始する必要はありません。 正常に完了したFutureを作成できます。

def multiply(multiplier: Int): Future[Int] =
  if (multiplier == 0) {
    Future.successful(0)
  } else {
    Future(multiplier * generateMagicNumber())
  }

または、失敗して完了したもの:

def divide(divider: Int): Future[Int] =
  if (divider == 0) {
    Future.failed(new IllegalArgumentException("Don't divide by zero"))
  } else {
    Future(generateMagicNumber() / divider)
  }

この条件付きの動作を単純化するために、 Future.fromTry()関数を使用できます。

def tryDivide(divider: Int): Future[Int] = Future.fromTry(Try {
  generateMagicNumber() / divider
})

Try {} ブロックに渡された式が例外をスローした場合、 Future.fromTry() Future.failed()関数と同等です。 それ以外の点では、 Future.successful()関数にいくらか似ています。

4. Await for Future

すでにFutureを作成しているので、その結果を待つ方法が必要です。

val maxWaitTime: FiniteDuration = Duration(5, TimeUnit.SECONDS)
val magicNumber: Int = Await.result(generatedMagicNumberF, maxWaitTime)

Await.result メインスレッドをブロックし、指定された結果が出るまで定義された期間待機します未来。 それ以降に準備ができていない場合、または障害が発生した場合は、 Await.result 例外をスローします

この例では、generatedMagicNumberFの結果を最大5秒間待機しています。

永遠に待ちたい場合は、Duration.Inf。を使用する必要があります

Await.resultはメインスレッドをブロックするため、本当に待つ必要がある場合にのみ使用する必要があります。 Future の結果を変換したり、他の結果と組み合わせたりする場合は、次のことができます。これは、ブロックしない方法で行います。これについては後で説明します。

5. コールバック

5.1. onComplete

Future の結果を待ってメインスレッドをブロックする代わりに、onCompleteメソッドを使用してコールバックを登録できます。

def printResult[A](result: Try[A]): Unit = result match {
  case Failure(exception) => println("Failed with: " + exception.getMessage)
  case Success(number)    => println("Succeed with: " + number)
}
magicNumberF.onComplete(printResult)

この場合、 Future は、 magicNumberF が正常に完了するか、失敗すると、printResultメソッドを実行します。

5.2. foreach

Future が正常に完了したときにのみコールバックを呼び出す場合は、foreachメソッドを使用する必要があります。

def printSucceedResult[A](result: A): Unit = println("Succeed with: " + result)
magicNumberF.foreach(printSucceedResult)

onComplete メソッドとは異なり、 magicNumberF が失敗して完了した場合、これは指定されたコールバック関数を実行しません。

これは、TryまたはOptionのforeachメソッドと同じ意味です。

6. エラー処理

6.1. 失敗

特定のFutureが適切な例外で失敗したかどうかを知ることに依存するユースケースがある場合、失敗を成功として扱います。 この目的のために、failedメソッドがあります。

val failedF: Future[Int] = Future.failed(new IllegalArgumentException("Boom!"))
val failureF: Future[Throwable] = failedF.failed

失敗したFutureを、結果としてThrowableで正常に完了したものに変換しようとします。 Future が正常に完了すると、結果のFutureNoSuchElementExceptionで失敗します。

6.2. fallbackTo

データベースからマジックナンバーを読み取ることができるDatabaseRepositoryと、最新のバックアップファイル(1日に1回作成)からマジックナンバーを読み取るFileBackupがあるとします。

trait DatabaseRepository {
  def readMagicNumber(): Future[Int]
  def updateMagicNumber(number: Int): Future[Boolean]
}
trait FileBackup {
  def readMagicNumberFromLatestBackup(): Future[Int]
}

この番号は私たちのビジネスにとって非常に重要であるため、データベースに問題が発生した場合に備えて、最新のバックアップからこの番号を読み取りたいと考えています。 この状況では、fallbackToメソッドを使用する必要があります。

trait MagicNumberService {
  val repository: DatabaseRepository
  val backup: FileBackup

  val magicNumberF: Future[Int] =
    repository.readMagicNumber()
      .fallbackTo(backup.readMagicNumberFromLatestBackup())
}

現在のFutureに障害が発生した場合は、代替の Future を使用して、それらを同時に評価します。 両方が失敗した場合、結果のFutureは失敗し、現在のものからThrowableが取得されます。

6.3. 回復

代替値を提供して特定の例外を処理する場合は、どのrecoverメソッドを使用する必要があります。

val recoveredF: Future[Int] = Future(3 / 0).recover {
  case _: ArithmeticException => 0
}

これは、一致する例外を成功した結果に変える部分関数を取ります。 それ以外の場合は、元の例外が保持されます。

6.4. restoreWith

別のFutureで特定の例外を処理する場合は、restoreの代わりにrecoverWithを使用する必要があります。

val recoveredWithF: Future[Int] = Future(3 / 0).recoverWith {
  case _: ArithmeticException => magicNumberF
}

7. トランスフォームフューチャー

7.1. マップ

Future インスタンスがある場合、 map メソッドを使用して、メインスレッドをブロックせずに成功した結果を変換できます。

def increment(number: Int): Int = number + 1
val nextMagicNumberF: Future[Int] = magicNumberF.map(increment)

それは新しいを作成します Future [Int] 適用することによってインクリメントの成功した結果への方法 magicNumberF。 それ以外の場合、新しいものにはと同じ例外が含まれます magicNumberF。

増分メソッドの評価は、暗黙的ExecutionContext。から取得された別のスレッドで行われます。

7.2. flatMap

Futureを返す関数を使用してFuture を変換する場合は、flatMapメソッドを使用する必要があります。

val updatedMagicNumberF: Future[Boolean] =
  nextMagicNumberF.flatMap(repository.updateMagicNumber)

map メソッドと同じように動作しますが、結果の Future をフラットに保ち、 Future[Future[Boolean]の代わりにFuture[Boolean]を返します。 ]]

flatMapおよびmapメソッドを使用すると、より理解しやすいコードを記述できるようになります。

7.3. 変換

map()関数とは対照的に、 transform(f:Try [T] => Try [S])関数を使用して、成功したケースと失敗したケースの両方をマップできます。

val value = Future.successful(42)
val transformed = value.transform {
  case Success(value) => Success(s"Successfully computed the $value")
  case Failure(cause) => Failure(new IllegalStateException(cause))
}

上記のように、 transform()メソッドは、 Future の結果を入力として受け取り、Tryインスタンスを出力として返す関数を受け入れます。 この場合、指定された Future[Int]Future[String]に変換しています。

このメソッドには、入力として2つの関数を受け取るオーバーロードバージョンがあります。1つは成功した場合、もう1つは失敗したシナリオです。

val overloaded = value.transform(
  value => s"Successfully computed $value", 
  cause => new IllegalStateException(cause)
)

最初の関数は成功した結果を別の関数にマップし、2番目の関数はスローされた例外をマップします。

7.4.  transformWith

transform()関数と同様に、 transformWith(f:Try [T] => Future [S])は関数を入力として受け入れます。 ただし、この関数は、指定されたTryインスタンスをFutureインスタンスに直接変換します。

value.transformWith {
  case Success(value) => Future.successful(s"Successfully computed the $value")
  case Failure(cause) => Future.failed(new IllegalStateException(cause))
}

上記のように、入力関数の戻りタイプは、TryではなくFutureです。

7.5. そして

andThen()関数は、指定された Future に副作用関数を適用し、同じFutureを返します。

Future.successful(42).andThen {
  case Success(v) => println(s"The answer is $v")
}

上に示したように、 andThen()はここで成功した結果を消費します。 andThen()関数は、指定された関数を適用した後に同じ Future を返すため、複数の andThen()呼び出しを連鎖させることができます。

val f = Future.successful(42).andThen {
  case Success(v) => println(s"The answer is $v")
} andThen {
  case Success(_) => // send HTTP request to signal success
  case Failure(_) =>  // send HTTP request to signal failure
}
  
f.onComplete { v =>
  println(s"The original future has returned: ${v}")
}

上に示したように、変数 f には、2つの副作用関数を適用した後も、元のFutureが含まれています。

8. Futureを組み合わせる

8.1. zip

2つの独立したFuturesの結果をペアに組み合わせるには、zipメソッドを使用する必要があります。

val pairOfMagicNumbersF: Future[(Int, Int)] =
  repository.readMagicNumber()
    .zip(backup.readMagicNumberFromLatestBackup())

両方のFuturesの成功した結果をペアに結合しようとします。 それらのいずれかが失敗した場合、結果のFutureも失敗します。それらの左端と同じ理由で。

8.2. zipWith

2つの独立したFuturesの結果をペア以外のものに結合する場合は、zipWithメソッドを使用する必要があります。

def areEqual(x: Int, y: Int): Boolean = x == y
val areMagicNumbersEqualF: Future[Boolean] =
  repository.readMagicNumber()
    .zipWith(backup.readMagicNumberFromLatestBackup())(areEqual)

両方の成功した結果を組み合わせるために与えられた関数を使用します先物。 

この例では、圧縮に areEqual メソッドを使用します。このメソッドは、(データベースとバックアップから取得した)両方の数値が等しいかどうかをチェックします。

8.3. トラバース

マジックナンバーのリストがあり、Publisherを使用してそれぞれを公開したいとします。

val magicNumbers: List[Int] = List(1, 2, 3, 4)
trait Publisher {
  def publishMagicNumber(number: Int): Future[Boolean]
}

この状況では、 Future.traverse メソッドを使用できます。このメソッドは、が複数の要素の並列マップを実行します

val published: Future[List[Boolean]] =
  Future.traverse(magicNumbers)(publisher.publishMagicNumber)

指定されたマジックナンバーごとにpublishMagicNumberメソッドを呼び出し、それらを1つのFutureに結合します。 これらの評価はそれぞれ、から取得した異なるスレッドで行われます。 ExecutionContext。

それらのいずれかが失敗すると、結果のFutureも失敗します。

9. 結論

この記事では、Scalaの Future APIについて説明しました。

Future を使用して非同期計算を開始する方法と、その結果を待つ方法を確認しました。 次に、メインスレッドをブロックせずにFutureの結果を変換および結合するいくつかの便利な方法を学びました。

最後に、成功した結果とエラーを処理する方法、および結果を組み合わせる方法を示しました。

いつものように、記事の完全なソースコードは、GitHubから入手できます。