问题
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:
- Are my aforementioned assumptions valid?
- Do Scala futures run on OS Threads or green threads? Or that depends on the execution context?
- 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
回答1:
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):
- Creates a
Function0[HttpResponse]
(I'll call it a thunk, for brevity) whoseapply
method issomeHttpClient.get("bar.com")
. IfsomeHttpClient
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. - Passes that thunk to
Future.apply
, which then: - Creates a
Promise[HttpResponse]
. - Schedules a task on the
ExecutionContext
passed implicitly toFuture.apply
. This task is to call the thunk: if the thunk successfully executes, thePromise
's associatedFuture
is completed (successfully) with the result of the thunk, otherwise thatFuture
fails (also a completion) with the thrownThrowable
(it may only fail if theThrowable
is matched byNonFatal
, I'm too lazy to check, in which case fatal throws are caught by theExecutionContext
). - As soon as the task is scheduled on the
ExecutionContext
(which may or may not be before the thunk is executed), theFuture
associated with thePromise
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
.
回答2:
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.
来源:https://stackoverflow.com/questions/63546030/how-do-scala-futures-operate-on-threads-and-how-can-they-be-used-to-execute-asy