-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Take non-Send resources out of World
#9122
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
base: main
Are you sure you want to change the base?
Conversation
hymm
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.
This is just a quick skim with comments on trying to shrink this pr a bit. Will dig more into the meat of the pr later.
SkiFire13
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.
// Insert channel resource into every sub-app. let (send, recv) = std::sync::mpsc::channel(); sub_apps.main.world.insert_resource(ThreadLocalAccessor::new(&mut tls, send.clone()); for sub_app in sub_apps.sub_apps.values_mut() { sub_app.world.insert_resource(ThreadLocalAccessor::new(&mut tls, send.clone()); }
FYI this example is unsound, each &mut tls technically invalidate any pointer obtained from the previous &mut tls.
2b08380 to
ed449db
Compare
| unsafe impl Sync for ThreadLocalChannel {} | ||
|
|
||
| /// Guards access to its thread-local data storage. | ||
| pub struct ThreadLocalStorage { |
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.
what's the advantage of the approach here vs building an abstraction around a global thread_local!()?
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.
The main benefit is this approach is siloed within an App. You could prepare multiple App instances on the same thread without them conflicting on the same global variable.
If thread_local! (or #[thread_local]) has any other notable caveats, they're not our problem. Maybe it'll have issues on console platforms and we just don't know about them yet.
Keep in mind that this setup is about making the same store of thread-local data "available on all threads". I think thread_local! could at most replace the mutex1. The rest of the implementation wouldn't change much. We still need the channel, etc.
Footnotes
-
But this mutex is only used to ensure soundness. It cannot be contended, so any performance impact is negligible. ↩
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 changed it to use thread_local! after all.
This has an annoying caveat where we kind of need to drop the non-send resources before exiting the app (or the user/plugin needs to do it) because of how thread_local! handles destructors.
|
I think we should swap this to the 0.13 milestone now. We're drawing close and this is still in draft. |
|
Let me see if I can have this ready for review this weekend. I've already rebased (just haven't pushed) and only the |
34091b1 to
4d8609b
Compare
… extra listener thread
8ea4c58 to
a8575de
Compare
a8575de to
a4a71e1
Compare
# Objective This is a necessary precursor to #9122 (this was split from that PR to reduce the amount of code to review all at once). Moving `!Send` resource ownership to `App` will make it unambiguously `!Send`. `SubApp` must be `Send`, so it can't wrap `App`. ## Solution Refactor `App` and `SubApp` to not have a recursive relationship. Since `SubApp` no longer wraps `App`, once `!Send` resources are moved out of `World` and into `App`, `SubApp` will become unambiguously `Send`. There could be less code duplication between `App` and `SubApp`, but that would break `App` method chaining. ## Changelog - `SubApp` no longer wraps `App`. - `App` fields are no longer publicly accessible. - `App` can no longer be converted into a `SubApp`. - Various methods now return references to a `SubApp` instead of an `App`. ## Migration Guide - To construct a sub-app, use `SubApp::new()`. `App` can no longer convert into `SubApp`. - If you implemented a trait for `App`, you may want to implement it for `SubApp` as well. - If you're accessing `app.world` directly, you now have to use `app.world()` and `app.world_mut()`. - `App::sub_app` now returns `&SubApp`. - `App::sub_app_mut` now returns `&mut SubApp`. - `App::get_sub_app` now returns `Option<&SubApp>.` - `App::get_sub_app_mut` now returns `Option<&mut SubApp>.`
# Objective I'm adopting #9122 and pulling some of the non controversial changes out to make the final pr easier to review. This pr removes the NonSend resource usage from `bevy_log`. ## Solution `tracing-chrome` uses a guard that is stored in the world, so that when it is dropped the json log file is written out. The guard is Send + !Sync, so we can store it in a SyncCell to hold it in a regular resource instead of using a non send resource. ## Testing Tested by running an example with `-F tracing chrome` and making sure there weren't any errors and the json file was created. --- ## Changelog - replaced `bevy_log`'s usage of a non send resource.
This PR builds on top of #9202 (I separated what could be separated from this PR into that one).
Objective
The aim of this PR is to be the "final" fix to our messy problems involving thread-local data (see #6552 for context).
Bevy only deals with a handful of thread-local resources (that mostly come from low-level OS APIs or libs that talk to those), but that handful has resulted in a ton of internal complexity (in
bevy_ecs,bevy_tasks, andbevy_app). The way we currently store them makes it either difficult or impossible to implement some nice-to-have features (e.g. generalized multi-world apps, framerate-independent input handling, etc.).Solution
tl;dr Separate non-
Senddata from the ECS entirely. It will remain indirectly accessible through an event loop running on the main thread. Then, move app updates into a separate thread (on permitting targets) so they don't block the event loop.This setup should be familiar to any audio plugin developers (or
asyncruntime developers). If a system on threadBneeds to do something with data that can't leave threadA, it can just send a command toA's event loop and wait forAto run it (and send any result back).Some unique benefits of separating
Sendand!Senddata like this are:ResandResMutcan be removed in favor ofRefandMutsince they're redundant withoutNonSendandNonSendMut.Worldbecomes unambiguouslySend, so a world can be dropped on any thread.Instant::now()) with virtually no delay/aliasing (the event loop isn't blocked).Changelog
TODO: finish this section
NonSendandNonSendMuthave been removed.Sendresources to an externalThreadLocalscontainer. It's normally owned by theApp.Migration Guide
If you were doing this before:
you're doing this now:
For better or worse, the handling that used to be buried inside the scheduler is now the responsibility of whoever writes an
Apprunner. It's not too much boilerplate though IMO. Here's an example of a basic runner that runs the app once.Spawning a thread like this is basically mandatory unless you're targeting an environment (i.e.
wasm32) or are enabling abevycompiler flag that forces completely single-threaded system execution. Otherwise, not doing so will risk running into a deadlock.Unfortunately, the channel between the main and app threads can't always be aI still had to box the channel as a trait object becausestd::sync::mpsc::channel. Thewinitrunner spawns an event loop that can only be woken up bywinitevents, so we have to use their specialEventLoopProxysender. That's the reason for theThreadLocalTaskSendertrait.bevy_ecsandbevy_appare separated.