Go back
Rust, Threads, and Spaghetti cover photo
← blog

/

writeup

Rust, Threads, and Spaghetti

3 min read

2022-02-03

I've always found concurrency a fascinating subject, because both the concept and how each language approaches it. Naturally, implementing the dining philosophers problem felt like the perfect excuse to explore rust's threading model and the infamous borrow checker in a real-world synchronization scenario.

Also, the "subject" for this implementation is taken directly from 1337's cursus, albeit it has different constraints to implement in C, you can check the subject here

The Idea Behind the Dining Philosophers

To the uninitiated, the dining philosophers problem is a concurrency puzzle that illustrates challenges in resource sharing. philosophers sit around a table, each needing two forks (left and right) to eat. If all grab the same fork first, they deadlock. If no one eats, they starve.

The Setup

Each philosopher is modeled as a thread, and forks are shared Arc<Mutex<_>> values passed into each thread's scope. The configuration (number of philosophers, sleep/eat/death timings) is parsed at runtime and passed via an Arc<Config> to every philosopher instance.

let forks: Vec<_> = (0..config.n_philo as u32)
    .map(|id| Arc::new(Mutex::new(Fork::new(id))))
    .collect();

philosophers spawn in a loop, each acquiring a left and right fork in a circular manner:

let fork_l_index = (philo_id - 1) as usize;
let fork_r_index = if philo_id == 1 {
    config.n_philo - 1
} else {
    (philo_id - 2) as usize
};

Each philosopher lives in a thread loop that tries to eat, then sleep, then think—or dies trying.

loop {
    if philo.eat().is_err() { break; }
    if philo.sleep().is_err() { break; }
    if philo.think().is_err() { break; }
}

The eating phase locks both forks using Mutex::lock, sleeps for t_eat, and updates the last_eat timestamp.

It's safe to note that this is a very basic, naive and verbose implementation, the goal here is not to optimize the solution as much as possible, but to illustrate the basics of Rust's concurrency primitives.

Touches & Hidden Gotchas

Shared Resource Modeling via Arc<Mutex<T>>

Rust forces you to be honest about ownership and mutability. Using Arc<Mutex<T>> means the forks are shared between threads with synchronized access. This is a common idiom in Rust for modeling shared mutable state in multithreaded programs.

However, locking order matters. Philosophers acquire the left fork and then the right - if every philosopher follows the same pattern, this could cause a circular wait and eventually a deadlock. That said, the risk is mitigated if thread scheduling varies enough that the probability of perfect contention is low. In real scenarios, introducing asymmetric fork grabbing or a "waiter" thread can eliminate the risk altogether.

Deadlock Detection? Not Yet

The program doesn't actively detect deadlocks or starvation. It assumes that a philosopher failing to eat in time will trigger is_dead() and exit the thread gracefully. The is_dead() logic compares last_eat + t_die with the current time and treats death as final - philosophers don’t get a retry.

let is_dead = now > last_eat + t_die;

Potential Improvements

Now, while I'm happy with this version that I wrote overnight while eating a weed gummy, there are a few areas I plan to revisit to harden the simulation:

1. Add a Deadlock Avoidance Strategy

Currently, every philosopher picks up forks in the same order. A simple enhancement would be to have one philosopher pick them in reverse (right then left) to break circular wait conditions. This asymmetry alone is enough to sidestep classic deadlock scenarios.

2. Learning Condvar

Rust’s std offers Condvar for blocking threads until a condition is met. Using it, or even std::sync::mpsc channels, could allow implementing a "waiter" thread that controls fork allocation, would be exiciting to learn more about those primitives and see where they can be applied to improve the simulation.

3. Async Implementation

I originally started this to specifically use kernel threads with no async runtime BUT, an alternative version could explore modeling the same behavior using async and tokio, allowing for more efficient cooperative multitasking and less reliance on kernel threads. Philosophers could yield to a runtime scheduler instead of blocking during sleep or eat.

Conclusion

Building this was both a dive into concurrency primitives and an exploration of how our big boi Rust makes you think twice about aliasing and shared mutability. Every part of this ownership of forks, temporal logic of death was a chance to appreciate the philosophy behind systems programming.

If you want to build or extend this, think of it less as a concurrency exercise and more like a platform.