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

Grid provider for Plain #77

Merged
merged 2 commits into from
Nov 16, 2023
Merged

Grid provider for Plain #77

merged 2 commits into from
Nov 16, 2023

Conversation

busstoptaktik
Copy link
Owner

@busstoptaktik busstoptaktik commented Nov 15, 2023

The GridCollection struct provides access to Arc<dyn Grid>, such that any grid needed by any Operator in any instantiation of Plain, always refer to the same heap allocation.

Unfortunately, this requires one line of unsafe{...} code.

We could have achieved almost-the-same, by building the collection into Plain herself. But this would also require that the InnerOp::new() methods switch to take a &mut Context, which may have cascading effects all through the code. Hence I prefer this solution.

@Rennzie do you have any opinions about this?

The GridCollection struct provides access to Arc<dyn Grid>, such that
any grid needed by any Operator in any instantiation of Plain, always
refer to the same meap allocation.
@Rennzie
Copy link
Contributor

Rennzie commented Nov 15, 2023

No strong opinions. It looks sensible to me.

For my own benefit, could you explain why the unsafe code is required?

@busstoptaktik
Copy link
Owner Author

busstoptaktik commented Nov 16, 2023

For my own benefit, could you explain why the unsafe code is required?

Not really sure why, but it does not compile without. This code:

unsafe { GRIDS.lock().unwrap().get_grid(name, self.paths.clone()) }

modifies (i.e. may modify) this static struct:

static mut GRIDS: Mutex<GridCollection> =
    Mutex::new(GridCollection(BTreeMap::<String, Arc<dyn Grid>>::new()));

which I believe should be (thread)-safe, due to the Mutex protection. But if I leave out the unsafe barrier, I get this message from the friendly compiler:

error[E0133]: use of mutable static is unsafe and requires unsafe function or block
   --> src\context\plain.rs:256:9
    |
256 |         GRIDS.lock().unwrap().get_grid(name, self.paths.clone())
    |         ^^^^^^^^^^^^ use of mutable static
    |
    = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior

...which is formally correct, but which I assumed I had mitigated by putting the access behind a Mutex

Plain now uses a global BTreeMap for holding
Arc<dyn Grid>, and hands out Arc::clone()s to
Ops needing access. This means that all uses
of a grid refers to the same heap allocation.

Unfortunately, we need to keep the "original"
instantiation in the list, so the Arcs will
never be fully released. To mitigate this, we
empty the BTreeMap every time a new Plain is
instantiated. While not ideal, it limits the
risk of hanging on to needless heap allocations
@busstoptaktik busstoptaktik merged commit 17d04f2 into main Nov 16, 2023
2 checks passed
@busstoptaktik busstoptaktik deleted the plain branch November 16, 2023 11:40
@Rennzie
Copy link
Contributor

Rennzie commented Nov 17, 2023

Ah interesting, thanks for those details @busstoptaktik.

I posed the question to Github Copilot which said:

The unsafe block is required here because you're dealing with a mutable static variable, GRIDS. In Rust, mutable statics are inherently unsafe due to potential data races, even when they're protected by a Mutex.

The Mutex does ensure that only one thread can access the data at a time, which prevents data races, but it doesn't change the fact that you're dealing with a mutable static. The Rust compiler doesn't know that you're using a Mutex to protect the data, so it still requires an unsafe block.

A bit more reading suggests that it's because it breaks the borrow checking rules of having exactly on mutable borrow at any point. I take it to mean that by having a global mut variable it basically prevents any other mut variable from being allowed.

Deeper down the rabbit hole I came across the once_cell crate which provides a Lazy type for working around this issue. I tried it out on a branch and I could remove the unsafe code with very few changes. See try-once-cell. I'd be curious to know if you see any short comings with this approach? I'm still very new to threaded programming and Web Workers come with safe guards built in so I've not ever dealt with this type of thing before.

@busstoptaktik
Copy link
Owner Author

Huge thanks, for the analysis and solution, @Rennzie!

I have merged & pushed this on main

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

Successfully merging this pull request may close these issues.

2 participants