-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Add synchronous ("parasitic") ExecutionContext #7784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "wip-callingthread-ec-\u221A"
Conversation
|
Seems like a good idea. Context switching can be hell, particularly when there are few resources available and it makes no sense to context switch for just a map etc ie a single core. I think I’d find myself using this in most places. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
CC @sjrd I vaguely remember we discussed this half a decade ago around the time scala-js/scala-js#2102 happened |
| * Nested submissions will be trampolined to prevent uncontrolled stack space growth. | ||
| * Any `NonFatal` or `InterruptedException`s will be reported to the `defaultReporter`. | ||
| * | ||
| * It is advised not to call any blocking code in the `Runnable`s submitted to this `ExecutionContext` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this warning honestly doesn't seem strong enough to me. is there any valid situation where you should call blocking code using this ExecutionContext?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally lean towards something like "DO NOT call any blocking code ..."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point, @NthPortal. I have updated the Scaladoc—what do you think of the new text?
| try submitForExecution(runnable) // User code so needs to be try-finally guarded here | ||
| catch { | ||
| case ie: InterruptedException => | ||
| reportFailure(ie) // TODO: Handle InterruptedException differently? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you catch an InterruptedException shouldn't you interrupt the current thread again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WellingR I've been back-and-forth on that one, since control is handed back to the "pool" already. Question is when it should be reset—since we cannot not execute any of the Runnables since then they will never be executed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@WellingR I just also remembered: If there's an InterruptedException thrown by user logic, they're using this EC to run blocking logic—which is documented to not be done. :/
|
I think it makes sense for making this public, I mean the current situation with I am also wondering whether having control over trampolining or not should be desired for those cases where you need to squeeze even more performance. |
| * advised to only execute logic which will quickly return control to the caller. | ||
| * | ||
| * DO NOT call any blocking code in the `Runnable`s submitted to this `ExecutionContext` | ||
| * as it will prevent progress by other enqueued `Runnable`s and the calling `Thread`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that the potential performance improvements from this over a thread pool executor are not mentioned - the last thing we want is people seeing this and saying "oh magic performance improvement secret sauce! I'm going to use this!" without reading the rest of the docs. Also, I wonder if we can be even clearer about the need for care. Something like "Symptoms of misusing this ExecutionContext include deadlocks and a potential for your application to slow to a crawl under minimal load."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jroper Great points, James. I'll craft a message about symptoms of misuse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jroper The new working name is parasitic—please check out the new docs. :)
| final object callingThread extends ExecutionContextExecutor with BatchingExecutor { | ||
| override final def submitForExecution(runnable: Runnable): Unit = runnable.run() | ||
| override final def execute(runnable: Runnable): Unit = submitSyncBatched(runnable) | ||
| override final def reportFailure(t: Throwable): Unit = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add explicit memory fences so that this execution context establishes the happens-before relationships that the normal one would?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's necessary.
There's no need for a memory barrier since it's guaranteed that the runnable will run on the calling thread, so there is an intrinsic happens before relationship there. So, as long as any state captured by the runnable is appropriately synchronised before being submitted to the executor (which this executor can't do anything about, memory barrier or not, if it's not), then everything should be fine.
So if we then think about this in the context of the Scala Future API, the DefaultPromise implementation uses AtomicReference.compareAndSet to query for completion state and/or add callbacks to be executed (eg in map) and remove them before executing them again (eg in complete). There is a memory barrier here (eg, on x86 its implemented using CMPXCHG which gives a memory barrier), so any state held by callbacks that then eventually get submitted to this executor will have their state synchronised so as to guarantee happens before before being submitted to the executor. Other implementations of Future likewise must implement a memory barrier on all their callback capturing methods, if they don't then adding a memory in this executor will only save them in a subset of situations, specifically, it couldn't save them, for example, if callbacks captured by map didn't have a memory barrier, since by the time this executor gets involved, the calling thread that registered that callback is gone and its memory cannot be synchronised.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
@lihaoyi Thanks for reminding me of that 5 year old conversation! :-) I guess I've grown more flexible in my thinking over the years :) |
66d4b6e to
e2aee62
Compare
| * as it will prevent progress by other enqueued `Runnable`s and the calling `Thread`. | ||
| * | ||
| * Symptoms of misuse of this `ExecutionContext` include, but are not limited to, deadlocks | ||
| * and severe performance problems. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jroper @NthPortal Is this an adequate explanation?
|
It would seem like there are basically only positive feedback on this proposal, so I have switched it over to Ready for review. |
|
I am a bit worried that this will set wrong expectations, not in terms of performance, but in terms of correctness. Asynchronous code, requesting This is something we've experienced in Scala.js with the |
|
@sjrd I think that's a good point. I think the name |
|
Something like
The wording can probably be improved, but the above should convey the message that I think is important. |
|
@sjrd Ok, thank you! Since ECs are not exclusive to Futures, I was struggling a bit to find the optimal place to add such documentation—but I guess it's fine to add it in the |
f71eb56 to
0205ca9
Compare
I have mixed feelings, but it took me a while to figure out what I wanted to say. First, there is the concern that where Second, it allows one to perform operations with an I am personally more concerned about the second problem, though both are important. I think both problems can be mostly addressed by good documentation, though I also wonder if it is possible or a good idea to make it somehow less accessible (a worse name? nested inside something?). I will try to review the docs more this weekend. |
|
@NthPortal Yes, I agree with everything you wrote. I think improving the documentation is the right thing to do. It is worth noting that none of the issues you mention is new capabilities (users can already write this logic themselves to starve or disrupt other ExecutionContexts). Perhaps with better documentation, and possibly another (more discouraging?) name, we can arrive at a solution which is to be deemed acceptable? |
What about
That description actually explains the mechanics and the problem with careless usage. |
|
I’d be ok with parasitic too. Has a name which leads the user to read its
docs.
--
Cheers,
√
|
|
I've committed an update to |
| * @return the global `ExecutionContext` | ||
| */ | ||
| def global: ExecutionContextExecutor = Implicits.global.asInstanceOf[ExecutionContextExecutor] | ||
| final def global: ExecutionContextExecutor = Implicits.global.asInstanceOf[ExecutionContextExecutor] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having trouble understanding why this reads from Implicits.global and does a cast, rather than Implicits.global reading from this and not needing to cast (not from this PR, ik)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NthPortal Great question! It's because Implicits.global needs to be typed ExecutionContext and not ExecutionContextExecutor otherwise its more specific type would take precedence over other implicit ExecutionContexts in scope. The non-implicit ExecutionContext.global can be typed as ExecutionContextExecutor so it can be used for Java APIs requiring Executor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@viktorklang what I meant was, why not instead do this?
lazy final val global: ExecutionContextExecutor = impl.ExecutionContextImpl.fromExecutor(null: Executor)
object Implicits {
implicit final def global: ExecutionContext = ExecutionContext.global // no cast needed!
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NthPortal Done!
| * WARNING: Do *not* call any blocking code in the `Runnable`s submitted to this `ExecutionContext` | ||
| * as it will prevent progress by other enqueued `Runnable`s and the calling `Thread`. | ||
| * In order to maximize application responsiveness, it is strongly advised | ||
| * to only execute logic which will quickly return control to the caller. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to say 'In order to maximize application responsiveness' and that it's 'strongly advised'? What are your thoughts on shortening the sentence to just 'Only execute logic which will quickly return control to the caller'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@NthPortal Updated! :)
NthPortal
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this approach. I think most people, upon seeing parasitic, will say "I don't think I want to use that one," which is what we want. There might be some shed painting on the exact phrasing of the docs, but overall looks great to me.
cc5ae2a to
310e54d
Compare
|
@viktorklang looks like messing with which is the primary definition of t8849.scala:8: error: ambiguous implicit values:
- both lazy value global in object Implicits of type => scala.concurrent.ExecutionContext
+ both method global in object Implicits of type => scala.concurrent.ExecutionContext |
|
@NthPortal Fixed t8849 |
|
Unless I missed something, documentation still doesn't seem to mention unpredictable outcomes when using this ExecutionContext through abstractions, i.e. I mean this bit:
Wording can be changed to be consistent with the rest of documentation, e.g. "latch on" -> "steal time". |
|
Piotr, I have updated the docs to include a mention about Futures. Have a
look!
--
Cheers,
√
|
|
Yep, looks good. |
|
Here's a bit of a wild idea: would it be possible to make an ExecutionContext that is smart enough to automatically trampoline operations on the same thread, IFF the operations are given the same execution context to run on (by reference equality), and where the ExecutionContext's differ then going through the normal flow? That would allow high performance in the common case where a bunch of |
|
@lihaoyi
Isn't that what you want? Other executors also have batching. I'm not sure how it works in detail, but IIUC a single batch is executed on single thread. |
This is a specific ExecutionContext which executes submitted Runnables on the calling thread, utilizing a combination of limited stack growth with on-heap trampolining to achieve its functionality and prevents unbounded stack growth. It is introduced as it is too easy for end-users to implement equivalent functionality but it is tricky to get right.
75d2433 to
bc11322
Compare
|
Squashed some of the commits here. Will merge after CI OKs it. |
|
@viktorklang people will find their way here from the release notes, so you may want to revise the PR description to reflect the final state |
|
@SethTisue Thanks Seth—I've revised the PR description. |
Changes from: * scala/scala#7784 * scala/scala#7062
|
new blog post by @wsargent about |
A synchronous, trampolining, ExecutionContext has been used for a long time within the Future implementation to run controlled logic as cheaply as possible.
I believe that there is a significant number of use-cases where it makes sense, for efficiency, to execute logic synchronously in a safe(-ish) way without having users to implement the logic for that ExecutionContext themselves—it is tricky to implement to say the least.
It is important to remember that
ExecutionContextshould be supplied via an implicit parameter, so that the caller can decide where logic should be executed. The use ofExecutionContext.parasiticmeans that logic may end up running on Threads/Pools that were not designed or intended to run specified logic. For instance, you may end up running CPU-bound logic on an IO-designed pool or vice versa. So use ofparasiticis only advisable when it really makes sense. There is also a real risk of hitting StackOverflowErrors for certain patterns of nested invocations where a deep call chain ends up in theparasiticexecutor, leading to even more stack usage in the subsequent execution. Currently theparasiticExecutionContext will allow a nested sequence of invocations at max 16, this may be changed in the future if it is discovered to cause problems.