Skip to content
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

Event loop implementation? #2333

Closed
armanbilge opened this issue Sep 9, 2021 · 10 comments
Closed

Event loop implementation? #2333

armanbilge opened this issue Sep 9, 2021 · 10 comments

Comments

@armanbilge
Copy link
Member

This has come up at least a couple times in different contexts and I think this would be a cool idea with some interesting applications. Plus @keynmol may have volunteered himself to take this on 😉

Why:

  • JVM: IO#unsafeRunSync in single-threaded environments (e.g. AWS lambda) without the penalty of creating threads or shifting.
  • JS: unsafeRunSync (!!) via a new "TemporalSyncIO" data type that implements Temporal and Sync. @djspiewak pointed out this would be similar to the microtask loop (with the addition of timers).
  • Native: when Native rolls around, IIUC this should give us a single-threaded runtime essentially for free.
@armanbilge
Copy link
Member Author

It occurred to me, another place this could be very useful is in fs2's sync compiler, which gets tripped up when used to run streams that need Concurrent. I think Ross suggested something like this in typelevel/fs2#2650 (comment).

See:
typelevel/fs2#2371
typelevel/fs2#2650
typelevel/fs2#2657

@bilki
Copy link

bilki commented Jan 6, 2022

Not sure whether it could be the solution for this use case, but I'm posting it, as we discussed on discord: while trying to work with some Java frameworks, like QtJambi or LWJGL, sometimes it's required for certain functions to be run on the main thread.

A workaround could be to execute what's needed to be run on the main thread before entering the IO context, then proceed with the rest of the program. But as soon as you need to run a function on the main thread again inside the IO, you're back to square one.

I don't have a repo to show the use case, but a sample snippet could look like more or less like:

package com.example

import cats.effect.{IO, IOApp}
import io.qt.widgets.*

object Sample extends IOApp.Simple:
  override val run =
    for
      app <- IO.delay(QApplication.initialize(Array.empty))
      _   <- // IO stuff running outside the main thread
      _   <- IO.delay(QMessageBox.information(null, "QtJambi", "Hello World"))
      _   <- // even more IO stuff running outside the main thread
      _   <- IO.delay(QApplication.shutdown)
    yield ()

@djspiewak
Copy link
Member

Crazy idea, but what if you could do something like .evalOn(IO.mainThread)?

@bilki
Copy link

bilki commented Jan 6, 2022

Yes, I was thinking exactly about that approach (best solution imho because of the fine granularity) when I faced the problem, but I didn't know if that could be achievable, or what the implications to the rest of the program would be even if that IO.mainThread existed 🤔

@djspiewak
Copy link
Member

I mean, it's doable in a couple different ways. The really easy one is made pretty clear by @vasilmkd's changes in #2724. While the application is running, the main thread is just chilling out in a queue.poll, waiting for the results. All you have to do is make it possible for that queue to contain a Runnable as well as the final results and then push through something that wraps it up as an ExecutionContext. The semantics of this then would be that mainThread would be a single-threaded executor where the worker thread is the main thread. As soon as the main fiber completes, its results would be picked up by the main thread which would then ignore any remaining work and produce the results just as today.

There are some details to work through though. Among other things, the syntax is likely to be worse, since IO.mainThread probably has to return an IO[ExecutionContext], and not a plain ExecutionContext, though we could make that nicer with an evalOnMain method. Additionally, the semantics of this method are unclear if the program is running under an explicit unsafeRun rather than IOApp. This is especially true if it's done as an unsafeRunAsync or unsafeRunAndForget, in which case we simply don't have a thread.

So I'm tempted to say that this is something which could really only be done within an IOApp. Maybe we should have an IOApp#MainThread: ExecutionContext which you can pass around explicitly if you need the functionality?

@bilki
Copy link

bilki commented Jan 7, 2022

There are some details to work through though. Among other things, the syntax is likely to be worse, since IO.mainThread probably has to return an IO[ExecutionContext], and not a plain ExecutionContext, though we could make that nicer with an evalOnMain method. Additionally, the semantics of this method are unclear if the program is running under an explicit unsafeRun rather than IOApp. This is especially true if it's done as an unsafeRunAsync or unsafeRunAndForget, in which case we simply don't have a thread.

So I'm tempted to say that this is something which could really only be done within an IOApp. Maybe we should have an IOApp#MainThread: ExecutionContext which you can pass around explicitly if you need the functionality?

Makes sense, the main thread is something closely related to the application entry point, so I'd expect to only be able to access it from within an IOApp, or the actual Scala application main. I've also seen that you drafted a PR implementing this approach (that was fast!).

@ritschwumm
Copy link

@bilki it's not just the main thread, with e.g. AWT (and by extension, swing) you sometimes need to run things synchronously in the event dispatch thread

@armanbilge
Copy link
Member Author

@ritschwumm actually it seems like it is already possible to run tasks on the AWT event dispatch thread without any changes in CE.

I see there is a method SwingUtilities.doLater that causes a Runnable "to be executed asynchronously on the AWT event dispatching thread."

So all you need is something like:

object AwtEventDispatchEC extends ExecutionContext {
  def execute(r: Runnable): Unit = SwingUtilities.doLater(r)
  def reportFailure(t: Throwable): Unit = t.printStackTrace()
}

IO(whatever).evalOn(AwtEventDispatchEC)

Since the main thread is special, it requires the changes proposed in #2732.

@djspiewak
Copy link
Member

doLater was actually a motivating example behind evalOn

@armanbilge
Copy link
Member Author

I think we're pretty much set with this. Besides #2732, my original "why"s no longer seem important.

  • JVM: the WSTP can now run blocking ops without shifting and will soon have timer capabilities as well. So, even in single-threaded environments getting onto the WSTP immediately is almost definitely optimal.
  • JS: TemporalSyncIO#unsafeRunSync is pretty esoteric :)
  • Native: will still need something, but that falls under Experiment with scala native #1302.

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

No branches or pull requests

4 participants