Suppose I write my own mutex class, containing data of some arbitrary type T and an AtomicBool with which I intend to synchronize access to the data:
struct MyMutex<T> {
in_use: AtomicBool,
data: UnsafeCell<T>
}
If the only way to access the data from any thread is spin-lock on the AtomicBool, is it well-defined to then read and write the data from multiple threads? In other words, is it safe and correct to implement a mutex in this way?
impl<T: Send> MyMutex<T> {
fn lock<'a>(&'a self) -> MyMutexGuard<'a, T> {
while !self.in_use.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst).is_ok() {
// spin
}
MyMutexGuard {
in_use: &self.in_use,
data: self.data.get()
}
}
}
struct MyMutexGuard<'a, T> {
in_use: &'a AtomicBool,
data: *mut T
}
impl<'a, T> Drop for MyMutexGuard<'a, T> {
fn drop(&mut self) {
if !self.in_use.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst).is_ok() {
panic!("Lock wasn't acquired properly");
}
}
}
impl<'a, T> Deref for MyMutexGuard<'a, T> { /* ---snip--- */ }
impl<'a, T> DerefMut for MyMutexGuard<'a, T> { /* ---snip--- */ }
unsafe impl<T: Send> Sync for MyMutex<T> {}
This approach seems to work, in the sense that I'm able to read and write the data from multiple threads on Rust Playground and don't seem to observe any panics or invalid intermediate states.
From a language theoretic point of view of course, "seeming to work" isn't very convincing. Is the data really protected from data races using this approach? Do I need to do extra work to tell the compiler how the data needs to be accessed in sequence with the atomic boolean? Could this break on a different architecture?
fn main() {
let data = "Hey earth".to_string();
let mutex1 = Arc::new(MyMutex::new(data));
let mutex2 = Arc::clone(&mutex1);
let mutex3 = Arc::clone(&mutex1);
thread::spawn(move || {
for _ in 0..100 {
let mut lock = mutex2.lock();
lock.push('!');
lock.push('!');
println!("thread1: {}", *lock);
thread::sleep(Duration::from_millis(1));
}
});
thread::spawn(move || {
for _ in 0..100 {
let mut lock = mutex3.lock();
lock.push('1');
lock.push('1');
println!("thread2: {}", *lock);
thread::sleep(Duration::from_millis(1));
}
});
for _ in 0..100 {
println!("main: {}", *mutex1.lock());
thread::sleep(Duration::from_millis(1));
}
}
Sample output:
main: Hey earth
thread2: Hey earth11
main: Hey earth11
thread1: Hey earth11!!
thread1: Hey earth11!!!!
main: Hey earth11!!!!
thread1: Hey earth11!!!!!!
main: Hey earth11!!!!!!
thread1: Hey earth11!!!!!!!!
thread1: Hey earth11!!!!!!!!!!
main: Hey earth11!!!!!!!!!!
thread1: Hey earth11!!!!!!!!!!!!
thread2: Hey earth11!!!!!!!!!!!!11
thread1: Hey earth11!!!!!!!!!!!!11!!
thread2: Hey earth11!!!!!!!!!!!!11!!11
main: Hey earth11!!!!!!!!!!!!11!!11
thread1: Hey earth11!!!!!!!!!!!!11!!11!!
thread2: Hey earth11!!!!!!!!!!!!11!!11!!11
thread1: Hey earth11!!!!!!!!!!!!11!!11!!11!!
thread1: Hey earth11!!!!!!!!!!!!11!!11!!11!!!!
main: Hey earth11!!!!!!!!!!!!11!!11!!11!!!!
[etc...]
More generally, I'm interested in whether this approach could be used to e.g. synchronize shared-read/exclusive-write access to separate entries of a shared array, but this smaller example seems like a necessary prerequisitive.