Learn Rust: Dining Philosophers runs not concurrent


#1

Today I’m started to learn Rust. So I came to https://doc.rust-lang.org/book/dining-philosophers.html
But the example won’t work correct. The philosophers won’t eat concurrent. I think there is an failure in the example, but I can’t figure out what.

Would be happy for any hint.


#2

It depends on your computer, and how fast it is. This example is being removed from the book in the next versions because of too many of these kinds of questions (not that you’re wrong to ask! Just that it’s confusing)


#3

Yeah I think you are right that they all eat in the same order. Because of the sleeps, I don’t think computer speed effects the outcome, less it’s a very very slow processor.

Because Michel is picking up the forks in reverse order, Emma is the first one who is able to grab both forks. When she’s done, Karl is unblocked, then Gilles is unblock, then Judith is unblock, then Michel is unblocked.

I guess what I’d like to see is add a random delay before the p.eat(&table) in the range of 1 second to cause anything interesting to happen, that way they are coming to the table at an undetermined time.


#4

Will it exist somewhere else or is it just completely removed. I think concurrency stuff is important, but it’s output is never deterministic AFAIK.


#5

I’m going to republish it on my blog, but it won’t be in the official docs anywhere.


#6

Yes, with the “thinking” part, it looks much more concurrent.
@steveklabnik: Maybe you can use something like this:

extern crate rand;

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

struct Philosopher {
    name: String,
    left: usize,
    right: usize,
}

impl Philosopher {
    fn new(name: &str, left: usize, right: usize) -> Philosopher {
        Philosopher {
            name: name.to_string(),
            left: left,
            right: right,
        }
    }

    fn work(&self, table: &Table) {
        thread::sleep(Duration::from_millis(
            rand::thread_rng().gen_range(1, 1001)
        ));
        self.eat(table)
    }

    fn eat(&self, table: &Table) {
        let _left = table.forks[self.left].lock().unwrap();
        thread::sleep(Duration::from_millis(150));

        let _right = table.forks[self.right].lock().unwrap();
        println!("{} is eating.", self.name);
        thread::sleep(Duration::from_millis(1000));

        println!("{} is done eating.", self.name);
    }
}

struct Table {
    forks: Vec<Mutex<()>>,
}

fn main() {
    let table = Arc::new(Table { forks: vec![
        Mutex::new(()),
        Mutex::new(()),
        Mutex::new(()),
        Mutex::new(()),
        Mutex::new(()),
    ]});

    let philosophers = vec![
        Philosopher::new("Judith Butler", 0, 1),
        Philosopher::new("Gilles Deleuze", 1, 2),
        Philosopher::new("Karl Marx", 2, 3),
        Philosopher::new("Emma Goldman", 3, 4),
        Philosopher::new("Michel Foucault", 0, 4),
    ];

    let handles: Vec<_> = philosophers.into_iter().map(|p| {
        let table = table.clone();

        thread::spawn(move || {
            p.work(&table)
        })
    }).collect();

    for h in handles {
        h.join().unwrap();
    }
}

#7

Yeah that looks like what I was thinking. I’m assuming the example didn’t do that because it uses an external crate and they wanted it to just use the standard library. However there is no random in the standard library so they’re kind of stuck. :slightly_smiling:


#8

This example is being removed from the book in the next versions because of too many of these kinds of questions

and because it is really verbose compared to C++ code :

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
using namespace std;
using Table = vector<mutex>;
struct Philosopher {
    string _name;
    unsigned _left;
    unsigned _right;
    void eat(Table & table) const {
        cout << _name << " is eating." << endl;
        lock_guard<mutex> lock_left(table[_left]);
        this_thread::sleep_for(150ms);
        lock_guard<mutex> lock_right(table[_right]);
        cout << _name << " is done eating." << endl;
        this_thread::sleep_for(1s);
    }
};
int main() {
    vector<Philosopher> philosophers { 
        {"Judith Butler", 0, 1},
        {"Gilles Deleuze", 1, 2},
        {"Karl Marx", 2, 3},
        {"Emma Goldman", 3, 4},
        {"Michel Foucault", 0, 4}
    };
    Table table(philosophers.size());
    vector<thread> threads;
    for (const auto & p : philosophers)
        threads.emplace_back([&p,&table](){p.eat(table);});
    for (auto & t : threads)
        t.join();
    return 0;
}

#9

Minimum lines of code was never the goal; showing off how features of Rust work together was.


#10

Whoa, I searched up this post for the exact same question, but I would certainly prefer the mild confusion over not seeing concurrency in the introduction at all. Isn’t concurrency one of Rust’s selling points anyway?


#11

After reading the first two examples in the Rust book, I was thinking “so Rust has the same features as C++ and is more verbose; next language please”.
I think the second example (dining philosophers) should present more interesting/unusual features. Maybe how OO and functional programming styles can work together.


#12

The question was asked on StackOverflow a month ago and I had some time on my hands so explored the behavior “in depth”: http://stackoverflow.com/questions/34395981/dining-philosophers-from-rust-documentation-do-not-eat-concurrently/34397886#34397886

Due to the way the program is structured, there are only 2 races:

  • a Judith/Michel race for Fork 0
  • an Emma/Michel race for Fork 4 if Michel one the Judith/Michel race

… leading to only 3 different orders:

  • Emma, Karl, Gilles, Judith, Michel (in case Judith wins Fork 0)
  • Emma, Michel, Karl, Gilles, Judith (in case Michel wins Fork 0 but loses Fork 4)
  • Michel, Emma, Karl, Gilles, Judith (in case Michel wins both Fork 0 and 4)

However, the execution tends to be very deterministic and unless one introduces random sleep times before the “left fork” lock one generally only sees a single order. And indeed, as the SO question mentioned, they do not eat concurrently.