3
votes

To my understanding, there are the 3 ways of doing IO in Scala, which I will try to express in pesudo code.

First, synchronous & blocking:

val syncAndBlocking: HttpResponse = someHttpClient.get("foo.com")

Here the main thread is just idle until the response is back..

Second, async but still blocking:

val asyncButBlocking: Future[HttpResponse] = Future { someHttpClient.get("bar.com") }

To my understanding, here the main thread is free (as Future executes on a separate thread) but that separate thread is blocked..

Third, asynchronous & non blocking. I am not sure how to implement that one, but to my best guess the implementation (eg. http client) itself has to be non-blocking, so something like:

val asynAndNotBlocking: Future[HttpResponse] = Future { someNonBlockingHttpClient.get("baz.com") }

My questions are:

  1. Are my aforementioned assumptions valid?
  2. Do Scala futures run on OS Threads or green threads? Or that depends on the execution context?
  3. In the third case where IO is async & non-blocking, how does that work under the hood? Does the thread only start the task (eg. send get request), and then becomes free again until it gets notified by some sort of an event loop when the response has arrived?

Question inspired by the following references: here & here

2
Your assumptions depend on how your ExecutionContext is defined. Your third example probably won't compile as someNoneBlocking... probably already returns a future. Here is a good overview of blocking/async/sync comaprison: github.com/slouc/concurrency-in-scala-with-ce Don't mind the cats-effect refrerencesSaskia
The third example should probably be just val asynAndNotBlocking: Future[HttpResponse] = someNonBlockingHttpClient.get("baz.com")Bergi

2 Answers

2
votes
val syncAndBlocking: HttpResponse = someHttpClient.get("foo.com")

This will block the calling thread until the response is received (as you note).

val asyncButBlocking: Future[HttpResponse] = Future { someHttpClient.get("bar.com") }

As with any call to Future.apply, this (at least conceptually, there may be optimizations which eliminate some steps):

  1. Creates a Function0[HttpResponse] (I'll call it a thunk, for brevity) whose apply method is someHttpClient.get("bar.com"). If someHttpClient is static, this could theoretically happen at compile time (I don't know off the top of my head if the Scala compiler will perform this optimization), otherwise it will happen at runtime.
  2. Passes that thunk to Future.apply, which then:
  3. Creates a Promise[HttpResponse].
  4. Schedules a task on the ExecutionContext passed implicitly to Future.apply. This task is to call the thunk: if the thunk successfully executes, the Promise's associated Future is completed (successfully) with the result of the thunk, otherwise that Future fails (also a completion) with the thrown Throwable (it may only fail if the Throwable is matched by NonFatal, I'm too lazy to check, in which case fatal throws are caught by the ExecutionContext).
  5. As soon as the task is scheduled on the ExecutionContext (which may or may not be before the thunk is executed), the Future associated with the Promise is returned.

The particulars of how the thunk is executed depend on the particular ExecutionContext and by extension on the particulars of the Scala runtime (for Scala on the JVM, the thunk will be run on a thread determined by the ExecutionContext, whether this is an OS thread or a green thread depends on the JVM, but OS thread is probably a safe assumption at least for now (Project Loom may affect that); for ScalaJS (since JavaScript doesn't expose threads) and Scala Native (as far as I know for now: conceivably an ExecutionContext could use OS threads, but there would be risks in the runtime around things like GC), this is probably an event loop with a global thread).

The calling thread is blocked until step 5 has executed, so from the caller's perspective, it's non-blocking, but there's a thread somewhere which is blocked.

val asynAndNotBlocking: Future[HttpResponse] = Future { someNonBlockingHttpClient.get("baz.com") }

...is probably not going to typecheck (assuming that it's the same HttpResponse type as above) since in order to be non-blocking the HttpResponse would have to be wrapped in a type which denotes asynchronicity/non-blocking (e.g. Future), so asyncAndNotBlocking is of type Future[Future[HttpResponse]], which is kind of a pointless type outside of a few specific usecases. You'd be more likely to have something like:

val asyncAndNotBlocking: Future[HttpResponse] = someNonBlockingHttpClient.get("baz.com")

or, if someNonBlockingHttpClient isn't native to Scala and returns some other asynchrony library, you'd have

val asyncAndNotBlocking: Future[HttpResponse] = SomeConversionToFuture(someNonBlockingHttpClient.get("baz.com"))

SomeConversionToFuture would basically be like the sketch above of Future.apply, but could, instead of using an ExecutionContext use operations in that other asynchrony library to run code to complete the associated Future when .get completes.

If you really wanted Future[Future[HttpResponse]] for some reason, given that it's likely that someNonBlockingHttpClient will return very quickly from .get (remember, it's asynchronous, so it can return as early as the request being set up and scheduled for being sent), Future.apply is probably not the way to go about things: the overhead of steps 1-5 may take longer than the time spent in .get! For this sort of situation, Future.successful is useful:

val doubleWrapped: Future[Future[HttpResponse]] = Future.successful( someNonBlockingHttpClient.get("baz.com"))

Future.successful doesn't involve a thunk, create a Promise, or schedule a task on the ExecutionContext (it doesn't even use an ExecutionContext!). It directly constructs an already-successfully-completed Future, but the value contained in that Future is computed (i.e. what would be in the thunk is executed) before Future.successful is called and it blocks the calling thread. This isn't a problem if the code in question is blocking for just long enough to setup something to execute asynchronously, but it can make something that's blocking for a long time look to the outside world like it's async/non-blocking.

Knowing when to use Future.apply and Future.successful is of some importance, especially if you care about performance and scalability. It's probably more common to see Future.successful used inappropriately than Future.apply (because it doesn't require an implicit ExecutionContext, I've seen novices gravitate to it). As Colin Breck put it, don't block your future success by improperly using Future.successful.

1
votes

A Future doesn't ”execute“ anywhere, because it is not a piece of runnable code – it is merely a way to access a result that may or may not yet be available.

The way you create a Future is by creating a Promise and then calling the future method on it. The Future will be completed as soon as you call the complete method on the Promise object.

When you create a Future using Future { doStuff() }, what happens is that a Promise is created, then doStuff starts executing in the ExecutionContext – usually that means it's running in a different thread. Then .future is called on the Promise and the Future is returned. When doStuff is done, complete is called on the Promise.

So conceptually, Futures and threads/ExecutionContexts are independent – there doesn't need to be a thread (green or otherwise) doing stuff for every incomplete future.