Skip to content

Commit e6f7b58

Browse files
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]>
2 parents 5de2689 + 17cf277 commit e6f7b58

File tree

5 files changed

+186
-33
lines changed

5 files changed

+186
-33
lines changed

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ gc_stats = []
1717
libc = "*"
1818
allocator = { path = "allocator", optional = true }
1919

20+
[dev-dependencies]
21+
lang_tester = "0.3"
22+
tempfile = "3.2"
23+
24+
25+
[[test]]
26+
name = "gc_tests"
27+
path = "gc_tests/run_tests.rs"
28+
harness = false
29+
2030
[build-dependencies]
2131
rerun_except = "0.1"
2232
num_cpus = "1.12"

gc_tests/run_tests.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use std::{env, path::PathBuf, process::Command};
2+
3+
use lang_tester::LangTester;
4+
use tempfile::TempDir;
5+
6+
fn deps_path() -> String {
7+
let mut path = PathBuf::new();
8+
path.push(env::var("CARGO_MANIFEST_DIR").unwrap());
9+
path.push("target");
10+
#[cfg(debug_assertions)]
11+
path.push("debug");
12+
#[cfg(not(debug_assertions))]
13+
path.push("release");
14+
path.push("deps");
15+
16+
path.to_str().unwrap().to_owned()
17+
}
18+
19+
fn main() {
20+
let rustc = env::var("RUSTGC").expect("RUSTGC environment var not specified");
21+
// We grab the rlibs from `target/<debug | release>/` but in order
22+
// for them to exist here, they must have been moved from `deps/`.
23+
// Simply running `cargo test` will not do this, instead, we must
24+
// ensure that `cargo build` has been run before running the tests.
25+
26+
#[cfg(debug_assertions)]
27+
let build_mode = "--debug";
28+
#[cfg(not(debug_assertions))]
29+
let build_mode = "--release";
30+
31+
Command::new("cargo")
32+
.args(&["build", build_mode])
33+
.env("RUSTC", &rustc.as_str())
34+
.output()
35+
.expect("Failed to build libs");
36+
37+
let tempdir = TempDir::new().unwrap();
38+
LangTester::new()
39+
.test_dir("gc_tests/tests")
40+
.test_file_filter(|p| p.extension().unwrap().to_str().unwrap() == "rs")
41+
.test_extract(|s| {
42+
Some(
43+
s.lines()
44+
.take_while(|l| l.starts_with("//"))
45+
.map(|l| &l[2..])
46+
.collect::<Vec<_>>()
47+
.join("\n"),
48+
)
49+
})
50+
.test_cmds(move |p| {
51+
let mut exe = PathBuf::new();
52+
exe.push(&tempdir);
53+
exe.push(p.file_stem().unwrap());
54+
55+
let mut compiler = Command::new(&rustc);
56+
compiler.args(&[
57+
"-L",
58+
deps_path().as_str(),
59+
p.to_str().unwrap(),
60+
"-o",
61+
exe.to_str().unwrap(),
62+
]);
63+
64+
vec![("Compiler", compiler), ("Run-time", Command::new(exe))]
65+
})
66+
.run();
67+
}

gc_tests/tests/multithreaded.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Run-time:
2+
// status: success
3+
#![feature(rustc_private)]
4+
5+
extern crate libgc;
6+
7+
use std::alloc::GcAllocator;
8+
use std::{thread, time};
9+
use std::sync::atomic::{AtomicBool, Ordering};
10+
use libgc::Gc;
11+
12+
#[global_allocator]
13+
static ALLOCATOR: GcAllocator = GcAllocator;
14+
15+
struct PanicOnDrop(String);
16+
17+
impl Drop for PanicOnDrop {
18+
fn drop(&mut self) {
19+
eprintln!("Finalizer called. Object erroneously collected");
20+
}
21+
22+
}
23+
24+
static mut NO_CHILD_EXISTS: AtomicBool = AtomicBool::new(true);
25+
26+
fn main() {
27+
for _ in 1..10 {
28+
thread::spawn(child);
29+
}
30+
31+
while(unsafe { NO_CHILD_EXISTS.load(Ordering::SeqCst) }) {};
32+
33+
// This should collect no garbage, because the call stacks of each child
34+
// thread should be scanned for roots.
35+
GcAllocator::force_gc();
36+
37+
// If there's a problem, a finalizer will print to stderr. Lets wait an
38+
// appropriate amount of time for this to happen.
39+
thread::sleep(time::Duration::from_millis(10));
40+
}
41+
42+
fn child() {
43+
unsafe { NO_CHILD_EXISTS.store(false, Ordering::SeqCst)};
44+
let gc = Gc::new(String::from("Hello world!"));
45+
46+
// Wait a bit before dying, ensuring that the thread stays alive long enough
47+
// cross the force_gc call.
48+
thread::sleep(time::Duration::from_millis(10));
49+
}
50+

src/gc.rs

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,39 @@ pub fn gc_init() {
3838
///
3939
/// `Gc<T>` automatically dereferences to `T` (via the `Deref` trait), so
4040
/// you can call `T`'s methods on a value of type `Gc<T>`.
41+
///
42+
/// `Gc<T>` will implement `Sync` as long as `T` implements `Sync`. `Gc<T>`
43+
/// will always implement `Send` because it requires `T` to implement `Send`.
44+
/// This is because if `T` has a finalizer, it will be run on a seperate thread.
4145
#[derive(PartialEq, Eq)]
42-
pub struct Gc<T: ?Sized + Send + Sync> {
43-
ptr: NonNull<GcBox<T>>,
46+
pub struct Gc<T: ?Sized + Send> {
47+
ptr: GcPointer<T>,
4448
_phantom: PhantomData<T>,
4549
}
4650

47-
impl<T: ?Sized + Unsize<U> + Send + Sync, U: ?Sized + Send + Sync> CoerceUnsized<Gc<U>> for Gc<T> {}
48-
impl<T: ?Sized + Unsize<U> + Send + Sync, U: ?Sized + Send + Sync> DispatchFromDyn<Gc<U>>
49-
for Gc<T>
51+
/// This zero-sized wrapper struct is needed to allow `Gc<T>` to have the same
52+
/// `Send` + `Sync` semantics as `T`. Without it, the inner `NonNull` type would
53+
/// mean that a `Gc` never implements `Send` or `Sync`.
54+
#[derive(PartialEq, Eq)]
55+
struct GcPointer<T: ?Sized>(NonNull<GcBox<T>>);
56+
57+
unsafe impl<T> Send for GcPointer<T> {}
58+
unsafe impl<T> Sync for GcPointer<T> {}
59+
60+
impl<T: ?Sized + Unsize<U> + Send, U: ?Sized + Send> CoerceUnsized<Gc<U>> for Gc<T> {}
61+
impl<T: ?Sized + Unsize<U> + Send, U: ?Sized + Send> DispatchFromDyn<Gc<U>> for Gc<T> {}
62+
63+
impl<T: ?Sized + Unsize<U> + Send, U: ?Sized + Send> CoerceUnsized<GcPointer<U>> for GcPointer<T> {}
64+
impl<T: ?Sized + Unsize<U> + Send, U: ?Sized + Send> DispatchFromDyn<GcPointer<U>>
65+
for GcPointer<T>
5066
{
5167
}
5268

53-
impl<T: Send + Sync> Gc<T> {
69+
impl<T: Send> Gc<T> {
5470
/// Constructs a new `Gc<T>`.
5571
pub fn new(v: T) -> Self {
5672
Gc {
57-
ptr: unsafe { NonNull::new_unchecked(GcBox::new(v)) },
73+
ptr: unsafe { GcPointer(NonNull::new_unchecked(GcBox::new(v))) },
5874
_phantom: PhantomData,
5975
}
6076
}
@@ -97,20 +113,20 @@ impl<T: Send + Sync> Gc<T> {
97113
}
98114

99115
pub fn unregister_finalizer(&mut self) {
100-
let ptr = self.ptr.as_ptr() as *mut GcBox<T>;
116+
let ptr = self.ptr.0.as_ptr() as *mut GcBox<T>;
101117
unsafe {
102118
GcBox::unregister_finalizer(&mut *ptr);
103119
}
104120
}
105121
}
106122

107-
impl Gc<dyn Any + Send + Sync> {
108-
pub fn downcast<T: Any + Send + Sync>(&self) -> Result<Gc<T>, Gc<dyn Any + Send + Sync>> {
123+
impl Gc<dyn Any + Send> {
124+
pub fn downcast<T: Any + Send>(&self) -> Result<Gc<T>, Gc<dyn Any + Send>> {
109125
if (*self).is::<T>() {
110-
let ptr = self.ptr.cast::<GcBox<T>>();
126+
let ptr = self.ptr.0.cast::<GcBox<T>>();
111127
Ok(Gc::from_inner(ptr))
112128
} else {
113-
Err(Gc::from_inner(self.ptr))
129+
Err(Gc::from_inner(self.ptr.0))
114130
}
115131
}
116132
}
@@ -125,14 +141,14 @@ pub fn needs_finalizer<T>() -> bool {
125141
std::mem::needs_finalizer::<T>()
126142
}
127143

128-
impl<T: ?Sized + Send + Sync> Gc<T> {
144+
impl<T: ?Sized + Send> Gc<T> {
129145
/// Get a raw pointer to the underlying value `T`.
130146
pub fn into_raw(this: Self) -> *const T {
131-
this.ptr.as_ptr() as *const T
147+
this.ptr.0.as_ptr() as *const T
132148
}
133149

134150
pub fn ptr_eq(this: &Self, other: &Self) -> bool {
135-
this.ptr.as_ptr() == other.ptr.as_ptr()
151+
this.ptr.0.as_ptr() == other.ptr.0.as_ptr()
136152
}
137153

138154
/// Get a `Gc<T>` from a raw pointer.
@@ -146,43 +162,43 @@ impl<T: ?Sized + Send + Sync> Gc<T> {
146162
/// size and alignment of the originally allocated block.
147163
pub fn from_raw(raw: *const T) -> Gc<T> {
148164
Gc {
149-
ptr: unsafe { NonNull::new_unchecked(raw as *mut GcBox<T>) },
165+
ptr: unsafe { GcPointer(NonNull::new_unchecked(raw as *mut GcBox<T>)) },
150166
_phantom: PhantomData,
151167
}
152168
}
153169

154170
fn from_inner(ptr: NonNull<GcBox<T>>) -> Self {
155171
Self {
156-
ptr,
172+
ptr: GcPointer(ptr),
157173
_phantom: PhantomData,
158174
}
159175
}
160176
}
161177

162-
impl<T: Send + Sync> Gc<MaybeUninit<T>> {
178+
impl<T: Send> Gc<MaybeUninit<T>> {
163179
/// As with `MaybeUninit::assume_init`, it is up to the caller to guarantee
164180
/// that the inner value really is in an initialized state. Calling this
165181
/// when the content is not yet fully initialized causes immediate undefined
166182
/// behaviour.
167183
pub unsafe fn assume_init(self) -> Gc<T> {
168-
let ptr = self.ptr.as_ptr() as *mut GcBox<MaybeUninit<T>>;
184+
let ptr = self.ptr.0.as_ptr() as *mut GcBox<MaybeUninit<T>>;
169185
Gc::from_inner((&mut *ptr).assume_init())
170186
}
171187
}
172188

173-
impl<T: ?Sized + fmt::Display + Send + Sync> fmt::Display for Gc<T> {
189+
impl<T: ?Sized + fmt::Display + Send> fmt::Display for Gc<T> {
174190
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175191
fmt::Display::fmt(&**self, f)
176192
}
177193
}
178194

179-
impl<T: ?Sized + fmt::Debug + Send + Sync> fmt::Debug for Gc<T> {
195+
impl<T: ?Sized + fmt::Debug + Send> fmt::Debug for Gc<T> {
180196
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181197
fmt::Debug::fmt(&**self, f)
182198
}
183199
}
184200

185-
impl<T: ?Sized + Send + Sync> fmt::Pointer for Gc<T> {
201+
impl<T: ?Sized + Send> fmt::Pointer for Gc<T> {
186202
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187203
fmt::Pointer::fmt(&(&**self as *const T), f)
188204
}
@@ -254,26 +270,33 @@ impl<T> GcBox<MaybeUninit<T>> {
254270
}
255271
}
256272

257-
impl<T: ?Sized + Send + Sync> Deref for Gc<T> {
273+
impl<T: ?Sized + Send> Deref for Gc<T> {
258274
type Target = T;
259275

260276
fn deref(&self) -> &Self::Target {
261-
unsafe { &*(self.ptr.as_ptr() as *const T) }
277+
unsafe { &*(self.ptr.0.as_ptr() as *const T) }
262278
}
263279
}
264280

265281
/// `Copy` and `Clone` are implemented manually because a reference to `Gc<T>`
266282
/// should be copyable regardless of `T`. It differs subtly from `#[derive(Copy,
267283
/// Clone)]` in that the latter only makes `Gc<T>` copyable if `T` is.
268-
impl<T: ?Sized + Send + Sync> Copy for Gc<T> {}
284+
impl<T: ?Sized + Send> Copy for Gc<T> {}
269285

270-
impl<T: ?Sized + Send + Sync> Clone for Gc<T> {
286+
impl<T: ?Sized + Send> Clone for Gc<T> {
271287
fn clone(&self) -> Self {
272288
*self
273289
}
274290
}
275291

276-
impl<T: ?Sized + Hash + Send + Sync> Hash for Gc<T> {
292+
impl<T: ?Sized> Copy for GcPointer<T> {}
293+
294+
impl<T: ?Sized> Clone for GcPointer<T> {
295+
fn clone(&self) -> Self {
296+
*self
297+
}
298+
}
299+
impl<T: ?Sized + Hash + Send> Hash for Gc<T> {
277300
fn hash<H: Hasher>(&self, state: &mut H) {
278301
(**self).hash(state);
279302
}
@@ -308,23 +331,23 @@ mod test {
308331
struct S2 {
309332
y: u64,
310333
}
311-
trait T: Send + Sync {
334+
trait T: Send {
312335
fn f(self: Gc<Self>) -> u64
313336
where
314-
Self: Send + Sync;
337+
Self: Send;
315338
}
316339
impl T for S1 {
317340
fn f(self: Gc<Self>) -> u64
318341
where
319-
Self: Send + Sync,
342+
Self: Send,
320343
{
321344
self.x
322345
}
323346
}
324347
impl T for S2 {
325348
fn f(self: Gc<Self>) -> u64
326349
where
327-
Self: Send + Sync,
350+
Self: Send,
328351
{
329352
self.y
330353
}

src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
#![cfg_attr(feature = "rustgc", feature(gc))]
1+
#![cfg_attr(not(feature = "standalone"), feature(gc))]
2+
#![cfg_attr(not(feature = "standalone"), feature(rustc_private))]
23
#![feature(core_intrinsics)]
34
#![feature(allocator_api)]
45
#![feature(alloc_layout_extra)]
@@ -23,7 +24,9 @@ pub mod stats;
2324
#[cfg(feature = "standalone")]
2425
pub use allocator::GcAllocator;
2526

27+
#[cfg(not(feature = "standalone"))]
28+
pub use std::alloc::GcAllocator;
29+
2630
pub use gc::Gc;
2731

28-
#[cfg(feature = "standalone")]
2932
pub static ALLOCATOR: GcAllocator = GcAllocator;

0 commit comments

Comments
 (0)