0

Suppose I have a future result, let's call it garfield

def garfield = Future{
  Thread.sleep(100)
  System.currentTimeMillis()
}

I can run garfield concurrently with in for comprehension like this

val f1 = garfield
val f2 = garfield

for {
  r1 <- f1
  r2 <- f2
} yield r1 -> r2

as explained in this answer.

Suppose I don't want to polluting the local scope with future variables if I won't need them later. Is this a valid alternative approach?

for {
  f1 <- Future{garfield}
  f2 <- Future{garfield}
  r1 <- f1
  r2 <- f2
} yield r1 -> r2

Edit

It appears my original approach, using Future.apply includes overhead that most of the time causes sequential execution, see example.

Using the alternative approach

for {
  f1 <- Future.successful(garfield)
  f2 <- Future.successful(garfield)
  r1 <- f1
  r2 <- f2
} yield r1 -> r2

behaves as expected.

Then again, this approach is a bit odd and perhaps a more conventional approach of scoping the futures in a Unit is preferred.

val res = {
  val f1 = garfield
  val f2 = garfield
  for {
    r1 <- f1
    r2 <- f2
  } yield r1 -> r2
}

I'm curios if someone could shed some more light on the reason for the sporadic lack of concurrent execution.

CervEd
  • 3,306
  • 28
  • 25

1 Answers1

1

For comprehension is sequential in principal, so no, this won't work.

Your code will sequentially evaluate f1 and then f2.

The following should work

(Updated with some changes from link from @ViktorKlang )

object FutureFor extends App {
  import concurrent.ExecutionContext.Implicits.global

  for {
    _ <- Future.unit
    f1 = Future { "garfield" }
    f2 = Future { "garfield" }
    r1 <- f1
    r2 <- f2
  } yield r1 -> r2

}

You have to start with <- to consume from "initial" future and it will decide the outcome type of for-comprehension.

The concurrency would be achieved with = as it will create Futures and then consume them with <-

But this is really confusing and I'd suggest to stick to

val f1 = garfield
val f2 = garfield

for {
  r1 <- f1
  r2 <- f2
} yield r1 -> r2

Edit:

Your approach Future { garfield() } does work and I missed the point that it is wrapping a Future.

And it is concurrent. See modified code that proves it:

import java.time.Instant
import java.util.concurrent.Executors

import scala.concurrent.{ExecutionContext, Future}
import scala.util.Random

object FutureFor extends App {
  private val random = new Random()
  implicit val ex =
    ExecutionContext.fromExecutor(Executors.newFixedThreadPool(10))

  def garfield() = Future {
    val started = Instant.now()
    Thread.sleep(random.nextInt(1000))
    val stopped = Instant.now()
    s"Started:$started on ${Thread.currentThread().getName}. Stopped $stopped"
  }

  val bar = Future
    .sequence {
      for {
        _ <- 1 to 10
      } yield
        for {
          f1 <- Future { garfield() }
          f2 <- Future { garfield() }
          r1 <- f1
          r2 <- f2
        } yield r1 + "\n" + r2
    }
    .map(_.mkString("\n\n"))
    .foreach(println)

  Thread.sleep(5000)
}

Prints:

Started:2020-04-24T13:23:46.043230Z on pool-1-thread-3. Stopped 2020-04-24T13:23:46.889296Z
Started:2020-04-24T13:23:46.428162Z on pool-1-thread-10. Stopped 2020-04-24T13:23:47.159586Z
....
Ivan Stanislavciuc
  • 7,140
  • 15
  • 18
  • Thanks, @Ivan. I'm really confused though because every so often it apears to execute concurrently https://scastie.scala-lang.org/U5Q1OWzRTNyL3s8bfXUnIA. You might have to repeat the run a few times. – CervEd Apr 24 '20 at 12:45
  • Hmmm... something strange is going on when I experiment with this. Maybe it's related to the overhead of calling `Future.apply` – CervEd Apr 24 '20 at 15:08
  • I've done some more experimenting https://scastie.scala-lang.org/XTMxslICRfKlQexADWCGcA and I'm puzzled by the results. Perhaps someone could share light on why alternative approach is sometimes sequential and sometimes concurrent – CervEd Apr 24 '20 at 16:43
  • My guess is that the overhead of `Future.apply` sometimes causes my alternative to execute sequentially. When I change to `Future.successful(garfield)`, execution is as expected. It also appears it might be faster than the standard way https://scastie.scala-lang.org/xKtHUMEVRDabuIfLUs3WBA – CervEd Apr 24 '20 at 17:05
  • 2
    @IvanStanislavciuc @CervEd It's important to distinguish "eligibility for concurrency" and "concurrent execution". Ivan's code above makes both `f1` and `f2` *eligible* for concurrent execution—how they are actually executed and in which order is completely up to the discretion of the `ExecutionContext` and ultimately to the OS Thread Scheduler. – Viktor Klang Apr 26 '20 at 20:41
  • @ViktorKlang, that's a fair point. My understanding is that all the approaches in my example would be eligible for concurrent execution with no guarantee in the order of execution of `f1` and `f2`, regardless of whether they are within the same iteration. Any sequential execution is inadvertent. Although wrapping `Futures` within a `Future.apply`, as I originally did, instead of in a `Future.successful` seems redundant. – CervEd Apr 27 '20 at 10:54
  • 2
    @CervEd The approach I recommend is the following: https://viktorklang.com/blog/Futures-in-Scala-protips-2.html – Viktor Klang Apr 27 '20 at 20:19
  • @ViktorKlang, I'm still a bit puzzled why it executes rather differently than the regular "assigning to `val` approach", the only difference being a blocking map call on a successful future. Then again, all things being concurrent I suppose it doesn't really matter – CervEd Apr 27 '20 at 20:26
  • @CervEd The difference is that `Future { Future {} }` doesn't do anything else but become Future[Future[_]] which happens almost instantaneously (it does not mean that the embedded Future has even started to execute yet) – Viktor Klang Apr 27 '20 at 20:28
  • @ViktorKlang, no I mean the approach both you and Ivan suggested, it executes rather differently than `val f1f = f1()` `for { f1 <- f1f ...`, which given that they desugar to almost the same thing puzzles me – CervEd Apr 27 '20 at 20:50
  • @CervEd Future.unit is already completed, and then the val-assignments are evaluated. – Viktor Klang Apr 27 '20 at 22:11
  • 1
    @ViktorKlang @CervEd Thanks for comments. I updated the answer with `unit` instead of `successful` – Ivan Stanislavciuc Apr 28 '20 at 07:29
  • @ViktorKlang, yeah no exactly. I think I'm just getting confused by the apparent pattern in the Scastie runtime system – CervEd Apr 28 '20 at 07:46
  • @ViktorKlang, the method you proposed, that should still work even if there was just one future, right? Ie `_ <- Future.unit; fv = Future{...}; v <- fv`. I know it doesn't make sense, but I just into an issue where things stopped working because I remove a future and I just want to make sure that's not expected behavior – CervEd May 03 '20 at 16:58
  • Yeah—it doesn't matter. But remember eligibility for concurrency does not mean guaranteed concurrency—that's up to the EC to decide. – Viktor Klang May 03 '20 at 17:14