-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 #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/alloy#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/alloy#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]>
- Loading branch information
Showing
5 changed files
with
186 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
use std::{env, path::PathBuf, process::Command}; | ||
|
||
use lang_tester::LangTester; | ||
use tempfile::TempDir; | ||
|
||
fn deps_path() -> String { | ||
let mut path = PathBuf::new(); | ||
path.push(env::var("CARGO_MANIFEST_DIR").unwrap()); | ||
path.push("target"); | ||
#[cfg(debug_assertions)] | ||
path.push("debug"); | ||
#[cfg(not(debug_assertions))] | ||
path.push("release"); | ||
path.push("deps"); | ||
|
||
path.to_str().unwrap().to_owned() | ||
} | ||
|
||
fn main() { | ||
let rustc = env::var("RUSTGC").expect("RUSTGC environment var not specified"); | ||
// We grab the rlibs from `target/<debug | release>/` but in order | ||
// for them to exist here, they must have been moved from `deps/`. | ||
// Simply running `cargo test` will not do this, instead, we must | ||
// ensure that `cargo build` has been run before running the tests. | ||
|
||
#[cfg(debug_assertions)] | ||
let build_mode = "--debug"; | ||
#[cfg(not(debug_assertions))] | ||
let build_mode = "--release"; | ||
|
||
Command::new("cargo") | ||
.args(&["build", build_mode]) | ||
.env("RUSTC", &rustc.as_str()) | ||
.output() | ||
.expect("Failed to build libs"); | ||
|
||
let tempdir = TempDir::new().unwrap(); | ||
LangTester::new() | ||
.test_dir("gc_tests/tests") | ||
.test_file_filter(|p| p.extension().unwrap().to_str().unwrap() == "rs") | ||
.test_extract(|s| { | ||
Some( | ||
s.lines() | ||
.take_while(|l| l.starts_with("//")) | ||
.map(|l| &l[2..]) | ||
.collect::<Vec<_>>() | ||
.join("\n"), | ||
) | ||
}) | ||
.test_cmds(move |p| { | ||
let mut exe = PathBuf::new(); | ||
exe.push(&tempdir); | ||
exe.push(p.file_stem().unwrap()); | ||
|
||
let mut compiler = Command::new(&rustc); | ||
compiler.args(&[ | ||
"-L", | ||
deps_path().as_str(), | ||
p.to_str().unwrap(), | ||
"-o", | ||
exe.to_str().unwrap(), | ||
]); | ||
|
||
vec![("Compiler", compiler), ("Run-time", Command::new(exe))] | ||
}) | ||
.run(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// Run-time: | ||
// status: success | ||
#![feature(rustc_private)] | ||
|
||
extern crate libgc; | ||
|
||
use std::alloc::GcAllocator; | ||
use std::{thread, time}; | ||
use std::sync::atomic::{AtomicBool, Ordering}; | ||
use libgc::Gc; | ||
|
||
#[global_allocator] | ||
static ALLOCATOR: GcAllocator = GcAllocator; | ||
|
||
struct PanicOnDrop(String); | ||
|
||
impl Drop for PanicOnDrop { | ||
fn drop(&mut self) { | ||
eprintln!("Finalizer called. Object erroneously collected"); | ||
} | ||
|
||
} | ||
|
||
static mut NO_CHILD_EXISTS: AtomicBool = AtomicBool::new(true); | ||
|
||
fn main() { | ||
for _ in 1..10 { | ||
thread::spawn(child); | ||
} | ||
|
||
while(unsafe { NO_CHILD_EXISTS.load(Ordering::SeqCst) }) {}; | ||
|
||
// This should collect no garbage, because the call stacks of each child | ||
// thread should be scanned for roots. | ||
GcAllocator::force_gc(); | ||
|
||
// If there's a problem, a finalizer will print to stderr. Lets wait an | ||
// appropriate amount of time for this to happen. | ||
thread::sleep(time::Duration::from_millis(10)); | ||
} | ||
|
||
fn child() { | ||
unsafe { NO_CHILD_EXISTS.store(false, Ordering::SeqCst)}; | ||
let gc = Gc::new(String::from("Hello world!")); | ||
|
||
// Wait a bit before dying, ensuring that the thread stays alive long enough | ||
// cross the force_gc call. | ||
thread::sleep(time::Duration::from_millis(10)); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters