Dining philosophers question

use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

#[derive(Clone)]
struct Philosopher {
    pub fork1: Arc<Mutex<usize>>,
    pub fork2: Arc<Mutex<usize>>,
    pub is_eating: bool,
    pub id: usize,
}

impl Philosopher {
    pub fn eat(&mut self) {
        {
            if let Ok(_) = self.fork1.lock() && let Ok(_) = self.fork2.lock() {
                self.is_eating = true;
                println!("Philosopher {} is eating!", self.id);
                thread::sleep(Duration::from_secs(1));
            }
        }
        self.is_eating = false;
    }
}

fn algo() {
    let forks: [Arc<Mutex<usize>>; 5] = core::array::from_fn(|x| Arc::new(Mutex::new(x)));
    let philosophers: [Arc<Mutex<Philosopher>>; 5] = core::array::from_fn(|x| Arc::new(Mutex::new(Philosopher {fork1: Arc::clone(&forks[x % 5]), fork2: Arc::clone(&forks[(x + 1) % 5]), is_eating: false, id: x})));
    let mut join_handles = Vec::new();
    for i in 0..5 {
        let value = philosophers.clone();
        let join_handle = thread::spawn(move || {
            let binding = Arc::clone(&value[i]);
            let mut philosopher = binding.lock().unwrap();
            loop {
                philosopher.eat();
            }
        });
        join_handles.push(join_handle);
    }
    for join_handle in join_handles {
        join_handle.join().unwrap();
    }
}

fn main() {
    algo();
}

The Dining Philosophers problem seems to have a string of locks followed by another.
Philosopher 1 is eating!
Philosopher 3 is eating!
Philosopher 1 is eating!
Philosopher 3 is eating!
Philosopher 3 is eating!
Philosopher 3 is eating!
Philosopher 3 is eating!
Philosopher 3 is eating!
Philosopher 3 is eating!
Philosopher 2 is eating!
Philosopher 2 is eating!
Philosopher 2 is eating!
Philosopher 2 is eating!
Philosopher 2 is eating!
Philosopher 2 is eating!
Philosopher 1 is eating!
Philosopher 1 is eating!
Philosopher 1 is eating!
I don't get why this happens.

Weirdly, it's this piece of code that causes it (it seems)

if let Ok(_) = self.fork1.lock() && let Ok(_) = self.fork2.lock() {
    self.is_eating = true;
    println!("Philosopher {} is eating!", self.id);
    thread::sleep(Duration::from_secs(1));
}

If I move the thread::sleep outside the if let statement, it works perfectly. Why?

During sleep and a call to lock on a mutex that is blocked, that thread is inactive.

Once sleep finishes that thread is active. The only significant action it performs is releasing the two locks and then reacquiring them on next loop.

The second inactive thread is woken when it has waited on the released mutex but this often takes more time than the first thread, so when active it discovers that the lock has already been taken again and goes inactive again.

What happens if you use if let Ok(_x) instead of if let Ok(_)? When you use a lone underscore no binding happens and the lock is dropped immediately.

2 Likes

By the way, I am very confused about what kind of problem Clippy has with this.

    Checking playground v0.0.1 (/playground)
error: this loop never actually loops
  --> src/main.rs:41:5
   |
41 | /     for join_handle in join_handles {
42 | |         join_handle.join().unwrap();
43 | |     }
   | |_____^
   |
help: this code is unreachable. Consider moving the reachable parts out
  --> src/main.rs:42:9
   |
42 |         join_handle.join().unwrap();
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#never_loop
   = note: `#[deny(clippy::never_loop)]` on by default
help: if you need the first element of the iterator, try writing
   |
41 -     for join_handle in join_handles {
41 +     if let Some(join_handle) = join_handles.into_iter().next() {
   |

error: could not compile `playground` (bin "playground") due to 1 previous error

How is the for loop unreachable?

Only in let is it none binding. _ in if let matches any type.

The second time around is impossible. Unwrap panics if the join result is Err. The Ok type is ! never

4 Likes

Oh yes, I made a mistake. Thank you! :face_with_open_eyes_and_hand_over_mouth:

Thanks for the correction.

Are you sure you are writing dining-philosopher?
Dining-philosophers is usually deadlocked, but in your case, philosophers are always eating

This is my version of dining-philosophers

use rand::Rng;
use std::sync::Arc;
use std::sync::Mutex;
use std::thread;
use std::thread::sleep;
use std::time;
use std::time::Duration;

const N: usize = 5;

fn main() {
    let v = {
        let mut v = vec![];
        for _ in 0..N {
            v.push(Arc::new(Mutex::new(())));
        }
        v
    };

    // for x in v.into_iter(){
    // println!("{:p}", x);
    // }

    let mut threads = Vec::with_capacity(N);
    for i in 0..N {
        let l = Arc::clone(&v[i]);
        let r = Arc::clone(&v[(i + 1) % N]);

        threads.push(thread::spawn(move || {
            loop {
                // thinking random seconds before eating
                let secs: u64 = rand::rng().random_range(1..=10);
                println!("{i} thinking for {} seconds...", secs);
                sleep(Duration::from_secs(secs));
                println!("{i} thinking Done");

                let x = l.lock().unwrap();
                println!("{} get left", i);

                let secs: u64 = rand::rng().random_range(1..=10);
                println!("{i} will eat {secs} seconds...");
                thread::sleep(time::Duration::from_secs(secs));

                let y = r.lock().unwrap();
                println!("{} start eat", i);

                println!("{} finish eat", i);
                // hold the lock
                // 避免提前释放
                // 虽然不加也可以
                let _ = *x;
                let _ = *y;
            }
        }));
    }

    // waiting for all thread terminate
    threads.into_iter().for_each(|thread| {
        thread
            .join()
            .expect("The thread creating or execution failed !")
    });
}

This was Dijkstra's solution I believe.

To avoid deadlock, it should be enough one philosopher doesn't pick forks like all the others. E.g. first left then right, if all the others do the other way around.