PRG (w5b): 並行計算と並列実行(Future計算)

Ken Wakita (https://prg1-2019.github.io/lecture/web/)

2019.11.8

前回の復習

Future 計算

Future {
  処理内容
  計算結果: T
} : Future[T]
  • Future オブジェクト:どこかで計算を実行し,いずれ計算が終わった暁には,その答えをくれるオブジェクト.

  • 計算結果の型が T のとき,Future オブジェクトの型は Future[T]

Future 計算の例

f()
Future { g() }
h()
  1. f() を実行する.

  2. g() を計算する Future オブジェクトを生成する.

    計算資源に余裕がある場合は(つまり,暇なプロセッサがあるとき)Future オブジェクトはすぐに g() の計算を開始する.

  3. (Future オブジェクトの計算を待たずに)h() の計算を始める.

    計算資源に余裕があり,g()h()の計算が重い場合はこれらの二つの計算は並列実行される.

同期と計算結果の受領

Await.result(<Future>, <時間>) によって、必要なら <Future> の計算を最大 <時間> だけ待ち、<Future>の計算結果を取得する。指定した時間内に計算が終わらない場合は TimeoutException 例外を発生する。

def future(): Unit = {
    val s = "Hello"
    val f: Future[String] = Future { List(s, " future!").reduce((s1, s2) => s1 + s2) }

    println(f"$s + ...: ${Await.result(f, Duration.Inf)}")
  }

同期と計算結果の受領

sbt:lx11> runMain Fib future
[info] Running (fork) Fib future
[info] Hello + ...: Hello future!
[success] Total time: 1 s
  • Future f は文字列のリストを連結する.

  • println メソッド中の Await.result(f, 最大待ち時間) が Future との同期と値の授受を実施する.

    1. Await.result(f, ...): f の計算を待つ

    2. Await.result(..., Duration.Inf) 永遠に待ち続ける

    3. Await.result(...) の結果は f の計算結果

Future計算の合成

Future計算の合成:合成の目的

  • ふたつの Future (f1f2) の並列実行を考える.

  • 最終的には f1f2 の計算結果を用いて計算したい.たとえば,分割統治法で大きな問題を複数の部分問題に分割したときに,それぞれの部分問題を Future として独立に並列計算し,それらの結果を合成して最終結果を得たい.

Note
  • 上の例は AND 並列

  • OR 並列はどうする?

Future計算の合成方法

for {
  v1 <- f1
  v2 <- f2
  ... }
  yield (v1, v2 を使った計算式)
}
  • f1, f2 が Future のとき for { v1 <- f1; v2 <- f2 } yield ... によって,複数の Future との同期と値の受理を記述できる.

  • f1, f2, … の計算がすべて完了し、v1, v2, … がすべて揃ったら yield ... によって,それらの結果をまとめあげた計算をができる。

Future計算の合成例

def add() {
    val f1: Future[Int] = Future { 1 }
    val f2: Future[Int] = Future { 2 }

    val sum: Future[Int] = for {
      v1 <- f1
      v2 <- f2
    } yield (v1 + v2)

    println(f"1 + 2 = ${Await.result(sum, Duration.Inf)}")
  }

Future計算の合成方法

sbt:lx11> runMain Fib add
[info] Running (fork) Fib add
[info] 1 + 2 = 3
[success] Total time: 1 s

Future計算の合成方法 (zip)

Future[T]::zip[U](that: Future[U]): Future[(T, U)]
  • f1:Future[T], f2:Future[U] のとき,f1.zip(f2)Future[(T, U)] を返す.

  • ふたつの Future を合成し、それぞれの計算結果(それぞれの型は T, U)の組(型は (T, U))を計算するFutureを返す.

Future計算の合成例 (zip)

  def add_zip() {
    val f1: Future[Int] = Future { 1 }
    val f2: Future[Int] = Future { 2 }

    val (v1, v2) = Await.result(f1.zip(f2), Duration.Inf)

    println(f"1 + 2 = ${v1 + v2}")
  }

Await.result(f1.zip(f2), ...)zip で合成した Future の計算を待って,f1, f2 の結果の組を得る。

zip で合成したFutureの実行例

sbt:lx11> runMain Fib add_zip
[info] Running (fork) Fib add_zip
[info] 1 + 2 = 3
[success] Total time: 1 s

Futureとの通信

Promise

  • Promise[T]Future[T] 型の Future オブジェクトの計算結果を保持するオブジェクト.

    Future にくっついている覗き穴のようなもの

  • Future が未了なら Promise の値は未了 (p.isComplete == false)

  • Future が正常終了してたら Promise の値は Success[T]

  • Future が以上終了してたら Promise の値は Failure[T]

Promise から見た Future

  • Promise p が割り当てられている Future は p.future

  • Future が Promise に値を通知する方法は p.success(計算結果)

    Scala では o.f(x)o f x と略記できるので、上の式を p success 計算結果 と書くことが多い。

Promise の利用例 (1/2)

def promise(): Unit = {
    val s = "Hello"

    val p = Promise[String]()
    Future { p success List(s, " future!").reduce((s1, s2) => s1 + s2) }

    println(f"Value from promise: ${Await.result(p.future, Duration.Inf)}")
  }
  • Promise p を作成

  • Future を作成し,計算 (List(...).reduce(...)) の結果を p success ... でPromiseに通知

Promise の利用例 (2/2)

def promise(): Unit = {
    val s = "Hello"

    val p = Promise[String]()
    Future { p success List(s, " future!").reduce((s1, s2) => s1 + s2) }

    println(f"Value from promise: ${Await.result(p.future, Duration.Inf)}")
  }
  • Await.result(p.future, ...) により,Promise p に結びついた Future と同期

  • 一見、無駄に Promise を使っているように見えるが,Future に一体化された計算と通信を分離することで柔軟性を増している.

    • Future での計算
    • Promise が表す通信路

Promiseを用いた計算の例

sbt:lx11> runMain Fib promise
[info] Running (fork) Fib promise
[info] Value from promise: Hello future!
[success] Total time: 1 s

Promiseを用いたFutureの合成例

def add_promise() {
    def zip(p1: Promise[Int], p2: Promise[Int]): Future[Int] = {
      for {
        v1 <- p1.future
        v2 <- p2.future
      } yield (v1 + v2)
    }

    val p1 = Promise[Int]()
    Future { p1 success 1 }

    val p2 = Promise[Int]()
    Future { p2 success 2 }

    println(f"1 + 2 = ${Await.result(zip(p1, p2), Duration.Inf)}")
  }

合成した Promise を用いた実行例

sbt:lx11> runMain Fib add_promise
[info] Running (fork) Fib add_promise
[info] 1 + 2 = 3
[success] Total time: 1 s

並列 Fibonacci 計算

再帰的 Fibonacci 計算

object recursive extends Fibonacci {
    def fib(n: Int): Int = {
      if (n <= 1) 1
      else fib(n-2) + fib(n-1)
    }
  }

再帰的 Fibonacci 計算の解剖

object recursive extends Fibonacci {
    def fib(n: Int): Int = {
      if (n <= 1) 1
      else fib(n-2) + fib(n-1)
    }
  }
  1. n <= 1 ならば 1 を返す.

  2. そうでなければ,

    1. fib(n-2) を計算する
    2. fib(n-2) の計算結果を受け取り,覚えておく(v1).
    3. fib(n-1) を計算する
    4. fib(n-1) の計算結果を受け取り,覚えておく(v2).
    5. v1v2 の和を計算する.(sum
    6. sum を返す.

再帰的 Fibonacci 計算の解剖のちょっと不明な点

  1. n <= 1 ならば 1 を返す.(誰に?

  2. そうでなければ,

    1. fib(n - 2) を計算する

    2. fib(n - 2) の計算結果を受け取り,覚えておく.(v1

    3. fib(n - 1) を計算する

    4. fib(n - 1) の計算結果を受け取り,覚えておく.(v2

    5. v1v2 の和を計算し,覚えておく.(sum

    6. sum を返す.(誰に?

並列 Fibonacci のアイデア

  • 誰に →「計算結果を渡す相手」を明示

  • 「計算結果を渡す相手」を Promise で表現

並列 Fibonacci (sum)

def sum(promise1: Promise[Int], promise2: Promise[Int], promise_sum: Promise[Int]) = {
      val (v1, v2) = Await.result(promise1.future.zip(promise2.future), Duration.Inf)
      promise_sum success (v1 + v2)
    }
  • 計算の引数を受け取る相手 promise1, promise2 promise1 と promise2 はそれぞれ fib(n-1), fib(n-2) を計算

  • 計算結果を渡す相手は promise_sum Promise たちの計算結果を収集したら promise_sum に合計値を伝える。

  • 板書で図示

並列 Fibonacci (fib)

(
    def fib(n: Int, p: Promise[Int]) {
      if (n <= 1) p success 1
      else {
        // fib(n-1), fib(n-2)を計算するFutureとPromise
        val promise1 = Promise[Int]() // fib(n-1) の結果を納める Promise
        val promise2 = Promise[Int]() // fib(n-2) の結果を納める Promise
        Future { fib(n-1, promise1) }
        Future { fib(n-2, promise2) }

        // fibを計算するFutureたちから結果を受け取りその和をpで待つFutureに伝えるFuture
        sum(promise1, promise2, p)
      }
    }
  )
  • 板書で図示