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

[feature] Constraining a future to a specific OS thread #1583

Closed
kstrafe opened this issue Sep 21, 2019 · 9 comments
Closed

[feature] Constraining a future to a specific OS thread #1583

kstrafe opened this issue Sep 21, 2019 · 9 comments
Labels
A-tokio Area: The main tokio crate C-feature-request Category: A feature request. M-task Module: tokio/task

Comments

@kstrafe
Copy link

kstrafe commented Sep 21, 2019

The Problem

Some mechanisms are closely tied to drivers or hardware events and require us to run said thing in a single thread.

An example of this is OpenGL. You first create something called an OpenGL context, which is tied to the thread it is created in. Then, you must only draw from that specific thread. Iirc the reason for this limitation is driver synchronization happening behind the scenes. This would be racy if OpenGL calls were invoked from several threads.

Another example is the winit crate, specifically on iOS. It uses cocoa as a back-end and this system has the limitation that only the main thread must handle input events. Doing otherwise would be an error.

What could be useful

It would be useful to constrain a future (and all its spawned children) to a specific OS thread. For instance through the following API:

tokio::spawn_constrained(std::thread::current().id(), async move {
   ... // This future and all its descendants will only run on the thread with the above id.
});

Why is it useful?

It allows for async winit and OpenGL awaits without causing low-level problems for drivers and/or hardware, allowing the thread to steal other tasks as long as winit, OpenGL, or anything else constrained to that thread is blocking.

@Ralith
Copy link
Contributor

Ralith commented Sep 21, 2019

Winit requires owning the event loop; as a result, running tokio on winit's thread doesn't make a lot of sense. You could still potentially use futures; I believe @Osspial is experimenting with adapting the core event loop to be futures-driven. Those futures could freely communicate with other tokio-owned threads, if desired.

@carllerche
Copy link
Member

This question comes up a bunch so I’m hoping to get a guide put together. It does not need to be extensive.

@Ralith
Copy link
Contributor

Ralith commented Sep 21, 2019

Relevant: https://github.com/osspial/winit-async

@hawkw
Copy link
Member

hawkw commented Nov 15, 2019

I believe that #1733 could potentially be used to constrain a future to run on the current thread. However, I don't think that would actually solve the winit problem?

@Osspial
Copy link

Osspial commented Nov 16, 2019

@hawkw Since writing winit-async, I've come to a better understanding of the intended use-case for the Future API, and how Winit's design parameters go against the flow of the current design. Long story short, Winit cannot have the overhead of an executor. When writing responsive desktop applications, you want your code to execute immediately in the OS's event loop thread, both so that you're working in grain with the OS API and so that you can modify pointers passed into the event loop by the OS. As winit-async shows, it's entirely possible to use use the current async/await API without pulling in an executor, but it's a bit of a PITA to implement properly. I don't think that limitation is inherent to async/await's design, and breaking the language features out of that mold shouldn't be too much of an extension of the Future API, but I need to take the time to fully form my thoughts on how you'd go about doing it.

@kstrafe
Copy link
Author

kstrafe commented Nov 16, 2019

@Osspial

Winit cannot have the overhead of an executor. When writing responsive desktop applications, you want your code to execute immediately in the OS's event loop thread,..

Does this include applications that are "non-responsive" (non-native gui, like games which render every 16 ms)? In the case of games, the standard solution appears to be pushing window events onto a channel for the rest of the system to handle.

Do you have the long story perhaps?

@Osspial
Copy link

Osspial commented Nov 16, 2019

@BourgondAries I suppose there's a bit of ambiguity around the term "responsive" in GUI development, heh. "Real-time applications" is probably a more accurate term for this purpose. Anyhow, I mean applications that have to (or really should) respond to user input immediately, and I include non-native GUI and games in that category. In terms of how rendering interacts with the threading system, games are a bit more lenient than desktop apps - desktop apps absolutely need to process redraw requests in the main thread so that the OS delays presenting until a frame is ready, which is necessary to achieve smooth window resizing. I believe using that model also nets some latency wins, though I haven't tested that personally.

Also, I mentioned events that need to modify a pointed-to value. The textbook example of this is WM_SIZING, which allows an application to intercept resize requests and modify the window size before the resize operation gets displayed on screen by modifying a pointer to the window's rectangle. This has to be processed exactly when dispatched by the OS, since the pointer is only valid for the lifetime of the event, which makes it impossible to cleanly handle via an executor. For a usage example off the top of my head, I believe ConEmu uses this to snap size increments to terminal character sizes.

I'll note that Winit's need for exclusive control over its event loop doesn't preclude using a thread pool within the larger application structure - indeed, driving a Tokio executor through a secondary thread, having a Rayon thread pool for speeding up calculations, or manually managing data processing through secondary threads is going to be a good idea in quite a lot of cases. It's just that Winit needs precise control over its thread, because the platform APIs we wrap demand precise control.

@Ralith
Copy link
Contributor

Ralith commented Nov 16, 2019

I think rather than the presence of an executor being unacceptable, that simply means that winit-async needs to be an executor, which is no surprise at all. What changes to the standard API do you see as necessary to accomplish this? It seems like it should work fine as-is to me.

@Darksonn Darksonn added A-tokio Area: The main tokio crate C-feature-request Category: A feature request. M-task Module: tokio/task labels Jul 25, 2020
@carllerche
Copy link
Member

I don't think we will add an explicit API to pin a future to an OS thread. There already are ways to do this. For example, you could run your own current-thread runtime on that OS thread.

Due to the lack of activity, I am going to close this issue. Please comment if this is incorrect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-tokio Area: The main tokio crate C-feature-request Category: A feature request. M-task Module: tokio/task
Projects
None yet
Development

No branches or pull requests

6 participants