You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
54: Fix the concurrency semantics for `Gc<T>`. r=ltratt a=jacob-hughes
This makes two major changes to the API:
1. It removes the requirement that `T: Sync` for `Gc<T>`.
2. It makes `Gc<T> : Send + Sync` if `T: Send + Sync`, fixing the ergonomic problems raised in softdevteam/libgc#49.
`Sync`'s purpose is to ensure that two threads can access the data in `T` in a thread-safe way. In other words, it implies that `T` has synchronisation guarantees. Originally, this was added as a constraint on `T` because any finalizer for `T` would run on a separate thread. However, it's now safe to remove this as a constraint because softdevteam#30 guarantees that a finalizer won't run early. This means that even without synchronized access, a race can't happen, because it's impossible for the finalizer to access `T`'s data while it's still in use by the mutator.
However, even though `Gc<T>` can now implement `Send` -- [thanks to multi-threaded collection support](softdevteam#31) -- `Gc<T>` still requires that `T: Send`, because `T` could be a type with shared ownership which aliases. This is necessary because off-thread finalization could mutate shared memory without synchronisation. An example using `Rc` makes this clear:
```rust
struct Inner(Rc<usize>);
fn foo() {
let rc = Rc::new(123);
{
let gc = Gc::new(Inner::new(Rc::clone(rc)));
}
// Assume `gc` is dead here, so it will be finalized in parallel on a separate thread.
// This means `Rc::drop` gets called which has the potential to race with
// any `Rc` increment / decrement on the main thread.
force_gc();
// Might race with gc's finalizer
bar(Rc::clone(rc));
}
```
Since finalizing any non-`Send` value can cause UB, we still disallow the construction of `Gc<T: !Send>` completely. This is certainly the most conservative approach. There are others:
- Not invoking finalizers for non-`Send` values. This is valid, since finalizers are not guaranteed to run. However, it's not exactly practical, it would mean that any `Gc<Rc<...>>` type structure would _always_ leak.
- Finalize `!Send` values on their mutator thread. This is really dangerous in the general case, because if any locks are held on the shared data by the mutator, this will deadlock (it's basically a variant of the async-signal-safe problem). However, if `T` is `!Send`, deadlocking is unlikely [although not impossible!], and could be an acceptable compromise. It's out of the scope of this PR, but it's something I've been playing around with.
Co-authored-by: Jake Hughes <[email protected]>
0 commit comments