Skip to content

Conversation

@viktorklang
Copy link
Contributor

@viktorklang viktorklang commented Feb 22, 2019

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 ExecutionContext should be supplied via an implicit parameter, so that the caller can decide where logic should be executed. The use of ExecutionContext.parasitic means 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 of parasitic is 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 the parasitic executor, leading to even more stack usage in the subsequent execution. Currently the parasitic ExecutionContext will allow a nested sequence of invocations at max 16, this may be changed in the future if it is discovered to cause problems.

@viktorklang viktorklang added this to the 2.13.0-RC1 milestone Feb 22, 2019
@viktorklang viktorklang self-assigned this Feb 22, 2019
@huntc
Copy link
Contributor

huntc commented Feb 22, 2019

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.

@viktorklang

This comment has been minimized.

@huntc

This comment has been minimized.

@viktorklang

This comment has been minimized.

@lihaoyi
Copy link
Contributor

lihaoyi commented Feb 23, 2019

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`
Copy link
Contributor

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?

Copy link
Contributor

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 ..."

Copy link
Contributor Author

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?
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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. :/

@mdedetrich
Copy link
Contributor

I think it makes sense for making this public, I mean the current situation with Future has gotten so ridiculous that akka ended up having to implement a "fast" Future to solve this exact problem (i.e. having to use an ExecutionContext for small .map operations).

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`.
Copy link
Contributor

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."

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 =
Copy link
Member

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?

Copy link
Contributor

@jroper jroper Feb 25, 2019

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@retronym @jroper Yes, since the execution does not leave the calling thread then there is no need for any fences.

@viktorklang
Copy link
Contributor Author

@lihaoyi Thanks for reminding me of that 5 year old conversation! :-) I guess I've grown more flexible in my thinking over the years :)

@viktorklang viktorklang force-pushed the wip-callingthread-ec-√ branch from 66d4b6e to e2aee62 Compare February 25, 2019 09:19
* 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.
Copy link
Contributor Author

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?

@viktorklang viktorklang marked this pull request as ready for review February 25, 2019 13:02
@viktorklang
Copy link
Contributor Author

It would seem like there are basically only positive feedback on this proposal, so I have switched it over to Ready for review.

@viktorklang viktorklang added performance the need for speed. usually compiler performance, sometimes runtime performance. and removed performance the need for speed. usually compiler performance, sometimes runtime performance. labels Feb 25, 2019
@sjrd
Copy link
Member

sjrd commented Feb 25, 2019

I am a bit worried that this will set wrong expectations, not in terms of performance, but in terms of correctness. Asynchronous code, requesting implicit ExecutionContexts, does not always follow the EC for all operations. Sometimes, they delegate to other asynchronous APIs, and provide callbacks in which they will complete Promises. Such code will still complete asynchronously, even if you call it with a callingThread EC. If a user calls such code with a callingThread EC and then expects the returned Future to be synchronously completed, their code won't work. It's even more annoying that the implementation could subtly change in later versions of a library, and then code that used to be OK by chance will break.

This is something we've experienced in Scala.js with the runNow execution context we used to have, and is one of the reasons we deprecated it, and removed it in 1.x.

@viktorklang
Copy link
Contributor Author

@sjrd I think that's a good point. I think the name runNow gives a bit different expectations than callingThread—but I agree that it should be better spelled out in the documentation for it. Do you have any suggestion as what would be the clearest formulation of such text?

@sjrd
Copy link
Member

sjrd commented Feb 25, 2019

Something like

Using callingThread does not give any guarantee that the resulting Future[T] will be completed by the time the callee returns. The caller must still use combinators and/or onComplete-like methods to process the result of the Future when it becomes ready.

The wording can probably be improved, but the above should convey the message that I think is important.

@viktorklang
Copy link
Contributor Author

@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 callingThread EC as it is most likely that it will be used for such code.

@viktorklang viktorklang force-pushed the wip-callingthread-ec-√ branch from f71eb56 to 0205ca9 Compare February 27, 2019 22:32
@NthPortal
Copy link
Contributor

It would seem like there are basically only positive feedback on this proposal

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 callingThread executes is non-deterministic. If you perform some operation on a Future, and it is already completed when the operation is performed, that operation will be executed on the current thread; otherwise, the operation will be executed by whatever ExecutionContext completes the Future. This non-determinism can lead to nasty bugs (as noted by Sébastien and others).

Second, it allows one to perform operations with an ExecutionContext they would not otherwise have access to. If the operation performed using callingThread is too expensive, it may slow or starve the ExecutionContext. While it would be possible to write your own implementation of callingThread and access the same inaccessible ExecutionContext, making it part of the standard library makes it significantly more accessible and more likely to be misused. If people have to implement it themselves, it will be mostly only experts who know when it's safe to use it implementing it; if it's in the standard library, anyone might think it's a good idea to use (when it isn't). This change makes it very easy to starve someone else's ExecutionContext.

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.

@viktorklang
Copy link
Contributor Author

@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?

@tarsa
Copy link

tarsa commented Feb 28, 2019

Perhaps with better documentation, and possibly another (more discouraging?) name, we can arrive at a solution which is to be deemed acceptable?

What about ExecutionContext.threadless or even ExecutionContext.parasitic? With documentation like:

This ExecutionContext latches on all threads it is called from. Threads are blocked until their thread local task queues are empty. When this ExecutionContext is used indirectly (through some higher level abstraction like Future) it can be unpredictable to which threads it will latch on. Possibly it can block threads that are critical to application stability and responsiveness.

That description actually explains the mechanics and the problem with careless usage.

@viktorklang
Copy link
Contributor Author

viktorklang commented Mar 2, 2019 via email

@viktorklang
Copy link
Contributor Author

I've committed an update to parasitic, have a look at it and the revised docs: cc5ae2a

* @return the global `ExecutionContext`
*/
def global: ExecutionContextExecutor = Implicits.global.asInstanceOf[ExecutionContextExecutor]
final def global: ExecutionContextExecutor = Implicits.global.asInstanceOf[ExecutionContextExecutor]
Copy link
Contributor

@NthPortal NthPortal Mar 2, 2019

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)

Copy link
Contributor Author

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.

Copy link
Contributor

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!
}

Copy link
Contributor Author

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.
Copy link
Contributor

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'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NthPortal Updated! :)

Copy link
Contributor

@NthPortal NthPortal left a 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.

@viktorklang viktorklang force-pushed the wip-callingthread-ec-√ branch from cc5ae2a to 310e54d Compare March 2, 2019 23:02
@NthPortal
Copy link
Contributor

@viktorklang looks like messing with which is the primary definition of global changed the phrasing of a partest output

  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

@viktorklang
Copy link
Contributor Author

@NthPortal Fixed t8849

@tarsa
Copy link

tarsa commented Mar 3, 2019

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:

When this ExecutionContext is used indirectly (through some higher level abstraction like Future) it can be unpredictable to which threads it will latch on. Possibly it can block threads that are critical to application stability and responsiveness.

Wording can be changed to be consistent with the rest of documentation, e.g. "latch on" -> "steal time".

@viktorklang
Copy link
Contributor Author

viktorklang commented Mar 3, 2019 via email

@tarsa
Copy link

tarsa commented Mar 3, 2019

Yep, looks good.
👍

@lihaoyi
Copy link
Contributor

lihaoyi commented Mar 6, 2019

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 maps and flatMaps share an ExecutionContext, while still preserving good behavior in the case where we're handing over our asynchronous workflows between different ExecutionContexts

@tarsa
Copy link

tarsa commented Mar 6, 2019

@lihaoyi
Documentation says:

Nested invocations of execute will be trampolined to prevent uncontrolled stack space growth.

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.

@viktorklang
Copy link
Contributor Author

@lihaoyi Yes, that is implemented in #7470 :-)

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.
@viktorklang viktorklang force-pushed the wip-callingthread-ec-√ branch from 75d2433 to bc11322 Compare March 7, 2019 17:53
@viktorklang
Copy link
Contributor Author

Squashed some of the commits here. Will merge after CI OKs it.

@SethTisue SethTisue changed the title FOR DISCUSSION: Synchronous (callingThread) ExecutionContext Add synchronous ("parasitic") ExecutionContext Mar 7, 2019
@SethTisue SethTisue added the release-notes worth highlighting in next release notes label Mar 7, 2019
@SethTisue SethTisue merged commit 1ab9e39 into scala:2.13.x Mar 7, 2019
@SethTisue
Copy link
Member

@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

@viktorklang
Copy link
Contributor Author

@SethTisue Thanks Seth—I've revised the PR description.

sjrd added a commit to sjrd/scala-js that referenced this pull request Mar 11, 2019
sjrd added a commit to sjrd/scala-js that referenced this pull request Mar 11, 2019
@diesalbla diesalbla added the library:concurrent Changes to the concurrency support in stdlib label Mar 15, 2019
@SethTisue
Copy link
Member

new blog post by @wsargent about ExecutionContext.global, ExecutionContext.parasitic, and ExecutionContext.opportunistic:

https://tersesystems.com/blog/2024/06/20/executioncontext.parasitic-and-friends/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

library:concurrent Changes to the concurrency support in stdlib release-notes worth highlighting in next release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.