Building a stupid Mutex in the Rust

Hey! Recently I had a little researching about how mutexes works. The result of my research is the new cargo crate fast-async-mutex. In this crate, I built a stupid async mutex and more complexity mutex, which check the locking ordering. And now I wont to write a series of posts about mutexes in the Rust. And the first article will about a maximum simple and stupid mutex which we can make.

By default, mutexes have two methods: acquire and release it also named as lock and unlock. When some thread calls a acquire method, the mutex will change its own state, and atomically set it to the acquired option. When other threads will try to acquire the mutex too, they will be blocked while the first thread will not call the realize method.

It’s one of the most popular ways to prevent data races in our apps.

Let’s create a stupid mutex

use std::sync::atomic::{AtomicBool, Ordering};#[derive(Debug, Default)]
struct Mutex {
is_acquired: AtomicBool,
}
impl Mutex {
fn acquire(&self) {
while !self.is_acquired.swap(true, Ordering::AcqRel) {
// wait
}
}

fn release(&self) {
self.is_acquired.store(false, Ordering::Release);
}
}

And now... Mutex is ready to use in production. Hehe

So, there we have 2 problems:

  • The while the loop will use CPU resources in idle. We need somebody like yield instruction, which tells your OS about the current thread may be stopped for some time.
  • We cannot get mutable access to our data. But we have unique access when the mutex is acquiring. Therefore we can use DerefMut trait to get mutable access to protected data.

Let’s signal the processor that it is inside a spin-loop

Rusts std lib has a spin_loop_hint method for doing that.

use std::sync::atomic::{AtomicBool, Ordering, spin_loop_hint};#[derive(Debug, Default)]
struct Mutex {
is_acquired: AtomicBool,
}
impl Mutex {
fn acquire(&self) {
while !self.is_acquired.swap(true, Ordering::AcqRel) {
spin_loop_hint() // Now we signals the processor that it is inside a busy-wait spin-loop
}
}

fn release(&self) {
self.is_acquired.store(false, Ordering::Release);
}
}

Making smart pointer from our stupid Mutex

use std::sync::atomic::{AtomicBool, Ordering, spin_loop_hint};
use std::cell::UnsafeCell;
use std::ops::{Deref, DerefMut};
struct Mutex<T> {
is_acquired: AtomicBool,
data: UnsafeCell<T>,
}
impl<T> Mutex<T> {
fn new(data: T) -> Mutex<T> {
Mutex {
is_acquired: AtomicBool::default(),
data: UnsafeCell::new(data),
}
}
fn acquire(&self) -> MutexGuard<'_, T> {
while !self.is_acquired.swap(true, Ordering::AcqRel) {
spin_loop_hint() // Now we signals the processor that it is inside a busy-wait spin-loop
}
MutexGuard { mutex: &self }
}
fn release(&self) {
self.is_acquired.store(false, Ordering::Release);
}
}
struct MutexGuard<'a, T> {
mutex: &'a Mutex<T>,
}
impl<T> Deref for MutexGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { &*self.mutex.data.get() }
}
}
impl<T> DerefMut for MutexGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { &mut *self.mutex.data.get() }
}
}
impl<T> Drop for MutexGuard<'_, T> {
fn drop(&mut self) {
self.mutex.release()
}
}
// We need to force Send and Sync traits because our mutex has
// UnsafeCell, which don't realize it
// As long as T: Send, it's fine to send and share Mutex<T> between threads.
unsafe impl<T> Send for Mutex<T> where T: Send {}
unsafe impl<T> Sync for Mutex<T> where T: Send {}
unsafe impl<T> Send for MutexGuard<'_, T> where T: Send {}
unsafe impl<T> Sync for MutexGuard<'_, T> where T: Send + Sync {}

Now our mutex store the data and give to your threads unique access to modify it without data races.

Let’s write the test

use std::thread::spawn;
use std::sync::Arc;
#[test]
fn test() {
let num = 100;
let mutex = Arc::new(Mutex::new(0));
let ths: Vec<_> = (0..num)
.map(|_| {
let mutex = mutex.clone();
spawn(move || {
let mut lock = mutex.acquire();
*lock += 1;
})
})
.collect();
for thread in ths {
thread.join().unwrap();
}

let lock = mutex.acquire();

assert_eq!(*lock, num)
}

Our stupid mutex works, nice!

Conclusion

I hope this article helps you to understand how mutexes maybe work. Of course, other mutexes in the std and other libs work much complexity. But there we look at what mutexes not to do any magic. They work simple.

So, if you want to know more, you can check sources of my own mutex crate fast-async-mutex.

Works sources of the current mutex with tests I saved on rust playground.

Also, I will write new articles about mutexes and concurrent programming on Rust if you will be interested.

Good luck!

Software Developer from VK