Skip to content
This repository has been archived by the owner on Jul 15, 2024. It is now read-only.

Consider adding async support #51

Open
notgull opened this issue Dec 17, 2022 · 7 comments
Open

Consider adding async support #51

notgull opened this issue Dec 17, 2022 · 7 comments

Comments

@notgull
Copy link

notgull commented Dec 17, 2022

Right now, the Application structure exposes a run() method that blocks indefinitely until the application finishes. It would be nice if there were a way to run a Future inside of this blocking loop, so that it would be possible to multiplex an asynchronous task with the GUI on one thread.

This would require changing the backend such that it can be woken up via a Waker. While you could just create a Waker to send an event to the loop, there is a more elegant way of doing this that can circumvent the normal event loops, based on the platform.

  • Windows lets you interrupt the message loop using MsgWaitForMultipleObjects.
  • With GTK, you can just register the future in the GLib event loop.
  • X11/Wayland can just be registered into epoll.
  • macOS requires some system hacks to get the Mach port belonging to GCD, but once you have it it can just be used in kqueue.
  • I'm not sure about web.

I'm willing to implement this behavior into the current system.

@raphlinus
Copy link
Contributor

raphlinus commented Dec 17, 2022

This is a deep topic. I've considered it but so far have not been convinced we should do this. You might be able to change my mind.

First, in general you don't want to run anything on the UI thread other than things that are required to process requests from the platform to the app (which are generally synchronous) or submit work to the platform. I believe everything else (including things like network and CPU-heavy async tasks) should be run in a reactor such as that provided by Tokio's runtime. Again, I am open to arguments otherwise.

Second, on Windows MsgWaitForMultipleObjects and friends have a fatal flaw, they don't run when resizing the window or in a modal dialog. This is why the run loop is very vanilla and integration with the Rust async world is by injecting user messages.

Third, one of the things that might change my mind is the way wgpu implements async. I'm not sure it's in it final form, it seems like there are a number of points where you explicitly poll (for example, to make async map of buffers actually happen). It would be nice to have this become plain Rust async, and expect the runtime to make progress as needed, whether that's on the UI thread or in another runtime. I don't understand all the issues around this (especially the plan for wgpu's end state) but would love to see a detailed design.

So I consider this a conversation. Thanks for engaging it.

@mwcampbell
Copy link
Collaborator

First, in general you don't want to run anything on the UI thread other than things that are required to process requests from the platform to the app (which are generally synchronous)

Depending on the platform, these synchronous requests may also include some or all accessibility API calls from the platform into the app. On macOS, all such calls run on the UI thread. On Windows, the WM_GETOBJECT message to request the root of the accessibility tree has to be handled on the UI thread. So far, I've avoided the need to restrict other UIA method calls (after that initial request) to the UI thread. I need to check on this, but I think the JAWS screen reader for Windows may do these other UIA method calls on the main thread anyway. And remember, accessibility APIs tend to be very chatty, requiring lots of method calls for a screen reader to collect information about whatever it's going to read next. So a busy UI thread can make the application feel especially unresponsive to a screen reader user.

@notgull
Copy link
Author

notgull commented Dec 17, 2022

Thank you for the response!

First, in general you don't want to run anything on the UI thread other than things that are required to process requests from the platform to the app (which are generally synchronous) or submit work to the platform. I believe everything else (including things like network and CPU-heavy async tasks) should be run in a reactor such as that provided by Tokio's runtime. Again, I am open to arguments otherwise.

The first thing that comes to mind is what if there isn't any thread other than the UI thread?. The web usecase comes to mind here, since the concurrency story for the web (web workers) is poor at the time of writing. There are also cases where there is an upper limit to the number of threads you can run at once; we've had to adapt to this in the past. Sometimes there isn't much of an option aside from running on the main thread.

Second, on Windows MsgWaitForMultipleObjects and friends have a fatal flaw, they don't run when resizing the window or in a modal dialog. This is why the run loop is very vanilla and integration with the Rust async world is by injecting user messages.

Wait, really? I mean, it's possible to just post messages to the thread queue to wake it up anyhow, so this shouldn't be much of an issue.

Third, one of the things that might change my mind is the way wgpu implements async. I'm not sure it's in it final form, it seems like there are a number of points where you explicitly poll (for example, to make async map of buffers actually happen). It would be nice to have this become plain Rust async, and expect the runtime to make progress as needed, whether that's on the UI thread or in another runtime. I don't understand all the issues around this (especially the plan for wgpu's end state) but would love to see a detailed design.

I'm not entirely familiar with wgpu, so someone else can correct me here. But it looks like most of the async blocks in wgpu either:

  • Immediately resolve using the ready() function, therefore bypassing the need for an async runtime at all.
  • Uses JS Promises as futures.

@notgull
Copy link
Author

notgull commented Dec 17, 2022

Another idea is that sometimes you have something you want to run on the UI thread. I'm designing a GUI framework where a thread local executor would run on the UI thread and manage widgets.

So a busy UI thread can make the application feel especially unresponsive to a screen reader user.

This scenario can be avoided by using yield points in the async block, which would prevent the UI thread from being blocked even under high contention.

@raphlinus
Copy link
Contributor

raphlinus commented Dec 17, 2022

The web is likely a special case, and deserves careful consideration, especially in conjunction with using wgpu. I know Dioxus has done some stuff with local executor, but haven't tracked down the detail.

On web, wgpu ultimately drills down to JS Promise, but running natively it's a Rust future. But (unless there is recent progress), those don't "just work," you also have to explicitly poll the device (see xilem app_main).

I am unexcited about yield points and consider the need for them a likely sign of an architectural flaw. Again I could have my opinion changed, but it would take persuasive evidence.

So, bottom line, I would love to see a design document that analyzed the requirements in different scenarios including better integration with wgpu and the web but would not be inclined to merge a PR without some kind of justification along those lines.

Also, this might be a good topic to discuss at office hours. Next one is 2023-01-03.

@notgull
Copy link
Author

notgull commented Dec 17, 2022

I am unexcited about yield points and consider the need for them a likely sign of an architectural flaw. Again I could have my opinion changed, but it would take persuasive evidence.

I mean, yes, it's a flaw in Rust's async ecosystem as a whole. tokio is actually better about this, since it uses cooperative task scheduling to ensure that tasks never run for too long without yielding. In other runtimes, it means that loops that can be called tightly (e.g. a loop in a web server) can starve other tasks. In this case, it would mean starving the event loop.

In my opinion, the potential benefits (e.g. being able to run async tasks using the UI thread as a root thread) outweigh those costs.

So, bottom line, I would love to see a design document that analyzed the requirements in different scenarios including better integration with wgpu and the web but would not be inclined to merge a PR without some kind of justification along those lines.

Also, this might be a good topic to discuss at office hours. Next one is 2023-01-03.

Sounds good, I'll see if I can prepare something before then.

@lord lord added the discussion label Apr 2, 2023
@notgull
Copy link
Author

notgull commented Apr 28, 2023

Since the last update, I've created the async-winit crate, which fulfills all of my needs when it comes to asynchronous GUI.

Still, I think it should be considered whether or not async is a good fit for glazier.

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

No branches or pull requests

4 participants