This commit is contained in:
nora 2023-01-10 19:11:29 +01:00
parent 13e6a1d6c6
commit afb32dbd6a
3 changed files with 153 additions and 7 deletions

View file

@ -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;

145
src/sendsync.rs Normal file
View file

@ -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. <maybe start without 'static?>
pub fn spawn<F: FnOnce() + 'static>(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<T> !Send for *const T {}
impl<T> !Send for *mut T {}
impl<T> !Send for UnsafeCell<T> {}
// 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<T> Send for Arc<T> {}
// 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<T: Send> Send for Arc<T> {}
// 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<T> !Sync for UnsafeCell<T> {}
// Also forbid pointers to make sure that unsafe datastructures have to manually assert syncness.
impl<T> !Sync for *const T {}
impl<T> !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<T> !Send for UnsafeCell<T> {}
// 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<T: Sync> 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<Cell<u8>> is therefore not Send, as this type is not thread-safe.
// unsafe impl<T: Sync> Send for Arc<T> {}
// 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: FnOnce()>(f: F) {
// Pretend that we're Send.
struct AssertSend<T>(T);
unsafe impl<T> std::marker::Send for AssertSend<T> {}
// 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)();
});
}

View file

@ -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);
}