[idea] JoinSend: A Scheme for Accessing Rc Structures Across Multiple Threads Using Mutex
Currently, data containing Rc is neither Sync nor Send, making it nearly impossible to use them in a multithreaded context, even when synchronized with Mutex.
However, intuitively, as long as the data structure containing Rc is treated as a whole, it should be feasible to send this whole structure across multiple threads.
Therefore, if we only need to create a wrapper type and isolate operations on its internal data: by allowing only closures that are Send, along with their return values, to access the internal data, we can preserve the integrity of the internal data and enable its transfer across multiple threads.
JoinSend: Marking Types Capable of Such Operation
/// Types that can be transferred to another thread when thread has already finished.
/// Usually, there is only a close connection to the current thread itself,
/// such as on some platforms [`std::sync::MutexGuard`] is bound to the thread,
/// so it cannot be passed to another thread even if the current thread finish.
pub auto trait JoinSend {}
impl<'a, T: ?Sized> !JoinSend for std::sync::MutexGuard<'a, T> {}
impl<'a, T: ?Sized> !JoinSend for std::sync::MappedMutexGuard<'a, T> {}
impl<'a, T: ?Sized> !JoinSend for std::sync::RwLockReadGuard<'a, T> {}
impl<'a, T: ?Sized> !JoinSend for std::sync::MappedRwLockReadGuard<'a, T> {}
impl<'a, T: ?Sized> !JoinSend for std::sync::RwLockWriteGuard<'a, T> {}
impl<'a, T: ?Sized> !JoinSend for std::sync::MappedRwLockWriteGuard<'a, T> {}
JoinCell: Maintaining the Encapsulation of Data Structures
/// A wrapper type that performs operations on its data by passing closures to an imaginary thread.
#[repr(transparent)]
pub struct JoinCell<T: ?Sized> {
value: T,
}
unsafe impl<T: JoinSend + ?Sized> Send for JoinCell<T> {}
unsafe impl<T: ?Sized> Sync for JoinCell<T> {}
impl<T> JoinCell<T> {
/// # Safety:
/// This value is the entire system, and no other data is synchronized with it.
pub const unsafe fn new_unchecked(value: T) -> Self {
Self { value }
}
/// We send this value to a virtual thread.
pub const fn new(value: T) -> Self
where
T: Send,
{
Self { value }
}
/// Imagine we send the inner value to a virtual thread
/// and then execute the closure on that thread
pub fn map<F, R>(self, f: F) -> JoinCell<R>
where
F: FnOnce(T) -> R,
F: Send,
{
JoinCell {
value: f(self.value),
}
}
pub fn new_by<F>(f: F) -> Self
where
F: FnOnce() -> T,
F: Send,
{
JoinCell::new(f).map(|f| f())
}
pub fn into_inner(self) -> T {
self.value
}
}
impl<T: ?Sized> JoinCell<T> {
/// We send the closure, execute it, and then send the result back.
pub fn with<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut T) -> R,
F: Send,
R: Send,
{
f(&mut self.value)
}
}
Example
struct Foo {
x: std::rc::Rc<i32>,
y: Option<std::rc::Rc<i32>>,
}
let foo = std::sync::Mutex::new(JoinCell::new_by(|| Foo {
x: std::rc::Rc::new(1),
y: None,
}));
std::thread::scope(|s| {
s.spawn(|| {
let mut guard = foo.lock().unwrap();
guard.with(|i| {
i.y = Some(i.x.clone());
});
});
});
let mut guard = foo.lock().unwrap();
guard.with(|i| {
assert_eq!(**i.y.as_ref().unwrap(), 1);
})