web-dev-qa-db-ja.com

Scala非同期/待機および並列化

Scalaでのasync/awaitの使い方について学んでいます。 https://github.com/scala/async でこれを読みました

理論的には、このコードは非同期(非ブロッキング)ですが、並列化されていません。

def slowCalcFuture: Future[Int] = ...             
def combined: Future[Int] = async {               
   await(slowCalcFuture) + await(slowCalcFuture)
}
val x: Int = Await.result(combined, 10.seconds)    

一方、これは並列化されています:

def combined: Future[Int] = async {
  val future1 = slowCalcFuture
  val future2 = slowCalcFuture
  await(future1) + await(future2)
}

それらの間の唯一の違いは、中間変数の使用です。これは並列化にどのように影響しますか?

37
Sanete

C#のasync & awaitに似ているため、いくつかの洞察を提供できます。 C#では、待つことができるTaskが「ホット」で返される、つまり既に実行されていることが一般的なルールです。 Scalaでも同じだと思います。関数から返されたFutureは明示的に開始する必要はなく、呼び出された後に「実行」されます。 notの場合、以下は純粋な(そしておそらく真実ではない)推測です。

最初のケースを分析しましょう:

async {
    await(slowCalcFuture) + await(slowCalcFuture)
}

そのブロックに到達し、最初の待機を行います。

async {
    await(slowCalcFuture) + await(slowCalcFuture)
    ^^^^^
}

さて、その計算が完了するのを非同期的に待っています。終了したら、ブロックの分析に進みます。

async {
    await(slowCalcFuture) + await(slowCalcFuture)
                            ^^^^^
}

2番目が待機しているため、2番目の計算が完了するまで非同期で待機しています。それが終わったら、2つの整数を加算して最終結果を計算できます。

ご覧のとおり、待機中はFuturesが1つずつ来るのを待って、段階的に進んでいます。

2番目の例を見てみましょう。

async {
  val future1 = slowCalcFuture
  val future2 = slowCalcFuture
  await(future1) + await(future2)
}

OK、それでこれが(おそらく)起こることです:

async {
  val future1 = slowCalcFuture // >> first future is started, but not awaited
  val future2 = slowCalcFuture // >> second future is started, but not awaited
  await(future1) + await(future2)
  ^^^^^
}

その後、最初のFutureを待っていますが、-両方の先物が現在実行されています。最初のものが戻ったとき、2番目はすでに完了している可能性があるため(結果をすぐに利用できるようになるでしょう)、もう少し待つ必要があるかもしれません。

これで、2番目の例が2つの計算を並行して実行し、両方の計算が完了するのを待つことが明らかです。両方の準備ができたら戻ります。最初の例では、ノンブロッキングで計算を実行しますが、順次実行します。

49
Patryk Ćwiek

patrykの答えは、少しに従うのが正しければ正解です。 async/awaitについて理解する主なことは、FutureflatMapを実行するもう1つの方法にすぎないということです。舞台裏には同時実行魔法はありません。非同期ブロック内のすべての呼び出しはシーケンシャルです。これには、実行スレッドを実際にブロックせず、残りの非同期ブロックをクロージャーでラップしてcallbackとして渡すawaitが含まれます。完了時Futureを待っています。そのため、最初のコードでは、2番目の計算は、それより前に開始されていないため、最初の待機が完了するまで開始されません。

24
Mikha

最初のケースでは、遅いスレッドを実行する新しいスレッドを作成し、1回の呼び出しでそれを待ちます。したがって、2番目の遅いフューチャーの呼び出しは、最初のスローフューチャーの完了後に実行されます。

2番目のケースでは、_val future1 = slowCalcFuture_が呼び出されると、新しいスレッドが効果的に作成され、「slowCalcFuture」関数へのポインターがスレッドに渡され、「実行してください」と示されます。スレッドプールからスレッドインスタンスを取得し、関数へのポインターをスレッドインスタンスに渡すのに必要な時間だけかかります。これは瞬時に考えることができます。したがって、_val future1 = slowCalcFuture_は「スレッドの取得とポインタの受け渡し」操作に変換されるため、すぐに完了し、次の行が遅延なく実行されます_val future2 = slowCalcFuture_。 Feauture 2も遅延なく実行される予定です。

_val future1 = slowCalcFuture_とawait(slowCalcFuture)の基本的な違いは、誰かにコーヒーを作るように頼むことと、コーヒーの準備ができるのを待つこととの違いと同じです。質問には2秒かかります:「コーヒーを作って頂けませんか?」というフレーズを言うのに必要です。しかし、コーヒーの準備ができるのを待つのに4分かかります。

このタスクの可能な変更は、最初の利用可能な回答を待っている可能性があります。たとえば、クラスター内の任意のサーバーに接続するとします。あなたが知っているすべてのサーバーに接続するためのリクエストを発行し、最初に応答するのはあなたのサーバーです。あなたはこれを行うことができます:Future.firstCompletedOf(Array(slowCalcFuture, slowCalcFuture))

0
Vadym Chekan