Kevin Meredith Kevin Meredith - 9 months ago 17
Scala Question

Task#apply versus Task#delay

Given the following

scala.concurrent.Task
instance created via
Task#delay
:

val t =
Task.delay { println(Thread.currentThread); Thread.sleep(5000); 42 }


I wrote a method that will run
t
asynchronously.

def f = t.runAsync {
case \/-(x) => println(x)
case -\/(e) => println(e.getMessage)
}


Running it shows that
f
evaluates entirely, i.e. waits 5 seconds, and then evaluates again. In other words, the second
f
appears to wait until the first
f
had completed

scala> {f; f; }
Thread[run-main-0,5,run-main-group-0]
42
Thread[run-main-0,5,run-main-group-0]
42


Then, I re-wrote
t
using
Task#apply
:

val u =
Task { println(Thread.currentThread); Thread.sleep(5000); 42 }


Again, I defined a method that executes
u
with
runAsync
:

def g = u.runAsync {
case \/-(x) => println(x)
case -\/(e) => println(e.getMessage)
}


Finally, I ran two
g
's.

scala> {g; g}
Thread[pool-3-thread-2,5,run-main-group-0]
Thread[pool-3-thread-3,5,run-main-group-0]

scala> 42
42


However, in the above result, the
g
's, more or less, ran at the same time.

I had expected that
{f; f; }
would've run asynchronously, i.e. in the same way as
g
. But, it seems to me that calling
f
resulted in a block.

EDIT

Task's docs note on
runAsync
:


Any pure, non-asynchronous computation at the head of this Future will be forced in the calling thread.


Since
t
's body is non-asynchronous, I suppose that the above comment explains why it blocked, i.e. "forced in the calling thread."

When is the right time to use
Task#delay
versus
Task#apply
?

Answer Source

You can think of Task.delay as a fancy version of something like () => Try[A]. It suspends evaluation of the computation, but doesn't have anything to say about what thread that evaluation is eventually going to run on, etc. (which means it's just going to run on the current thread).

This is often exactly what you want. Consider a definition like this:

val currentTime: Task[Long] = Task.xxx(System.currentTimeMillis)

We can't use now because that would evaluate the time immediately (and only once, on definition). We could use apply, but forcing an asynchronous boundary for this computation is wasteful and unnecessary—we actually want it to run in the current thread, just not right now. This is exactly what delay provides.

In general when you're modeling your computations, if something is always going to be computationally expensive, you might want to consider Task.apply, which means the evaluation will always happen on a thread determined by the current implicit ExecutorService. This may make usage a little cleaner, at the expense of flexibility—you're baking something you know about the runtime characteristics of the evaluation of the computation into its definition.

The nice thing about using delay to define your asynchronous computations is that you can always force an asynchronous boundary by wrapping your Task with Task.fork, which gets you essentially the same thing you'd have if you'd defined the computation with Task.apply. It's not possible to go in the other direction—if you use Task.apply, the implicit strategy is going to determine where the computation is evaluated and that's all there is to it.