diff --git a/src/lib.rs b/src/lib.rs index 240bdc8..7028ba3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,9 @@ #![feature(ptr_metadata)] #![feature(trace_macros)] +#![feature(auto_traits)] +#![feature(negative_impls)] -mod cfg_match; -mod unroll_int; -mod unsized_clone; - -pub use unsized_clone::*; +pub mod cfg_match; +pub mod sendsync; +pub mod unroll_int; +pub mod unsized_clone; diff --git a/src/sendsync.rs b/src/sendsync.rs new file mode 100644 index 0000000..d39f1ed --- /dev/null +++ b/src/sendsync.rs @@ -0,0 +1,145 @@ +use std::{ + cell::{Cell, UnsafeCell}, + rc::Rc, + sync::Arc, +}; + +// Today we want to design the safe `std::thread::spawn` function and the traits around that. + +// First we have the following signature. + +pub fn spawn(f: F) { + // SAFETY: Well, that's what we're here for today. + unsafe { magic_unchecked_spawn_for_our_convenience(f) } +} + +#[test] +fn send_over_integer() { + // This is perfectly safe. No data is shared. Our function allows this, which is very nice. + + let x = 0; + spawn(move || drop(dbg!(x))); +} + +#[test] +fn rc_the_new_contender() { + // Now, let's send over a more complex type like an Rc. + let x = Rc::new(0); + let x2 = x.clone(); + spawn(move || { + x2.clone(); + }); + x.clone(); // DATA RACE +} + +// Oh no, we have a data race. This is not exactly good, in fact it's really bad. +// So, how can we forbid Rc from being sent over? +// We need some kind of "this can be sent to other threads" trait. Let's call it "Send". +pub unsafe auto trait Send {} +// It's an auto trait because we really don't want everyone having to implement this manually. +// It's also unsafe because the safety of our spawn function relies on it. + +// Why exactly was Rc able to trigger a data race here? The key lies in interior mutability. +// Interior mutability like Cells but also raw pointers should therefore be forbidden by default. +impl !Send for *const T {} +impl !Send for *mut T {} +impl !Send for UnsafeCell {} + +// When we now add a F: Send bound to our spawn function, the Rc stops cinoukubgè + +#[test] +fn but_arc_is_fine() { + // Now, let's send over a more complex type like an Rc. + let x = Arc::new(0); + let x2 = x.clone(); + spawn(move || { + x2.clone(); + }); + x.clone(); +} + +// Arc is fine here because it uses atomics internally. But it fails to compile! Here, Arc (or us in this case) +// needs to assert that it's fine: +unsafe impl Send for Arc {} + +// So now, everything is good. + +#[test] +fn an_arc_of_sadness() { + let x = Arc::new(Cell::new(0)); + let x2 = x.clone(); + spawn(move || { + x2.set(0); + }); + x.set(1); // DATA RACE +} + +// Oh, not quite. We have an issue. Luckily it's a simple one, we just forgot to put a `T: Send` bound +// on the impl. + +// unsafe impl Send for Arc {} + +// After we fix this, it fails to compile as desired. + +#[test] +fn i_am_just_sending_over_a_cell() { + // We just send the Cell over and only ever use it from the other thread. + // This is perfectly sound. We want to allow this. + + let x = Cell::new(0); + spawn(move || { + let x = x; + x.set(1) + }); +} + +// The example above fails to compile. But there is no unsoundness here, we want to allow this. +// But as we've seen above, we cannot make `Cell: Send`. + +// Really, we have two concepts at play here +// - Something that we can send owned top a thread. +// - Something that we can send a reference of to another thread +// Rc can support neither of those, as its possibly unsoundness (clone) can be triggered just +// with a shared reference to it, but also with an owned Rc because two owned Rcs can point to the same memory. +// Cell is different. Having a &Cell across threads can lead to the data race. But having an owned Cell cannot +// trigger the unsoundness, as it will just mutate the local value. + +// Let's add a new trait for types that support being shared behind a reference. + +pub unsafe auto trait Sync {} + +// UnsafeCell is the key here and will make sure that types like Cell are !Sync. +impl !Sync for UnsafeCell {} + +// Also forbid pointers to make sure that unsafe datastructures have to manually assert syncness. +impl !Sync for *const T {} +impl !Sync for *mut T {} + +// Now we can actually implement Send for UnsafeCell again. Sending a Cell-like type to another thread +// is not problematic, only sharing it is. +// -impl !Send for UnsafeCell {} + +// Now we just need one last piece, the interactions of Send and Sync. +// Sync means that we can share a reference across a thread, so let's represent that in an impl. +unsafe impl Send for &T {} + +// The same "reference like behavior" applies to Arc. We are only allowed to Send an Arc to another thread +// if the thing it holds is Sync. Arc> is therefore not Send, as this type is not thread-safe. +// unsafe impl Send for Arc {} + +// In general, anything that provides shared access to T needs a T: Sync bound on its Send impl. + +// Bonus: The cursed impl of magic_unchecked_spawn_for_our_convenience. +pub unsafe fn magic_unchecked_spawn_for_our_convenience(f: F) { + // Pretend that we're Send. + struct AssertSend(T); + unsafe impl std::marker::Send for AssertSend {} + + // Get over the annoying 'static requirement by just sending over an erased pointer and reading from it. + let s = Box::into_raw(Box::new(f)); + let p = AssertSend(s.cast::<()>()); + std::thread::spawn(|| { + let p = unsafe { Box::from_raw({ p }.0 as *mut F) }; + (p)(); + }); +} diff --git a/src/unroll_int.rs b/src/unroll_int.rs index 43e191c..f9f421a 100644 --- a/src/unroll_int.rs +++ b/src/unroll_int.rs @@ -32,6 +32,6 @@ create_unroll_int! { 10 9 8 7 6 5 4 3 1 2 0 } -fn x() { - let x = unroll_int!(20); +pub fn x() { + let _ = unroll_int!(20); }