-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Take non-Send
resources out of World
#9122
base: main
Are you sure you want to change the base?
Conversation
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.
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 |
9a6cda7
to
34091b1
Compare
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-
Send
data 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
async
runtime developers). If a system on threadB
needs to do something with data that can't leave threadA
, it can just send a command toA
's event loop and wait forA
to run it (and send any result back).Some unique benefits of separating
Send
and!Send
data like this are:Res
andResMut
can be removed in favor ofRef
andMut
since they're redundant withoutNonSend
andNonSendMut
.World
becomes 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
NonSend
andNonSendMut
have been removed.Send
resources to an externalThreadLocals
container. 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
App
runner. 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 abevy
compiler 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
. Thewinit
runner spawns an event loop that can only be woken up bywinit
events, so we have to use their specialEventLoopProxy
sender. That's the reason for theThreadLocalTaskSender
trait.bevy_ecs
andbevy_app
are separated.