来自Rust文档的餐饮哲学家不会同时吃

时间:2015-12-21 12:42:22

标签: concurrency rust

我试图关注Rust文档中的dining philosophers example。链接的最终代码:

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

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 eat(&self, table: &Table) {
        let _left = table.forks[self.left].lock().unwrap();
        thread::sleep_ms(150);
        let _right = table.forks[self.right].lock().unwrap();

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

        thread::sleep_ms(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.eat(&table);
        })
    }).collect();

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

运行它会产生以下输出:

Michel Foucault is eating.
Michel Foucault is done eating.
Emma Goldman is eating.
Emma Goldman is done eating.
Karl Marx is eating.
Karl Marx is done eating.
Gilles Deleuze is eating.
Gilles Deleuze is done eating.
Judith Butler is eating.
Judith Butler is done eating.

根据文献记载,哲学家应该能够同时吃饭。期望的结果是这样的:

Gilles Deleuze is eating.
Emma Goldman is eating.
Emma Goldman is done eating.
Gilles Deleuze is done eating.
Judith Butler is eating.
Karl Marx is eating.
Judith Butler is done eating.
Michel Foucault is eating.
Karl Marx is done eating.
Michel Foucault is done eating.

不幸的是,无论代码执行的频率如何,都不会发生这种情况。

我目前在Windows上使用rustc 1.5.0 (3d7cd77e4 2015-12-04),但问题也出现在Rust操场上。随意try it yourself

2 个答案:

答案 0 :(得分:5)

由于拣货叉之间的睡眠,问题的实施和建议的输出不匹配。

我不确定为什么 Michel Foucault 总是先启动(可能是线程调度的工作方式),但其余部分很容易解释。

由于抓住主手叉和副手叉之间的暂停(*),有两个阶段:

  • 阶段1:抢你的主叉
  • 阶段2:抓住你的副手

第1阶段后:

  • Fork 0掌握在Michel Foucault或Judith Butler手中
  • Fork 1掌握在Gilles Deleuze手中
  • Fork 2掌握在Karl Marx手中
  • Fork 3掌握在Emma Goldman手中

现在,请注意,只有Fork 4可用于抓取!

我们在第2阶段有两个案例:

a)朱迪思抓住了福克斯0  b)Michel抓住了Fork 0

从(a)开始:

  • 所有哲学家都被封锁,除了抓住Fork 4的Emma
  • 当Emma完成后,她释放了Fork 3,Karl立即抓住了
  • 当卡尔完成......
  • 最后,Judith完成了,她释放了Fork 0,而Michel吃了

在案例(a)中,只有一位哲学家可以在任何特定时间吃东西。

注意:我强迫这个案子暂停了Michel 150ms,然后让他抓住他的第一把叉子。

案例(b)更复杂,因为我们再一次参加比赛,这次是在Emma和Michel之间抓住Fork 4.我们是绅士,所以Emma会先走,Michel抓住Fork 4的案例现在被命名(C):

  • Emma抓住Fork 4,所有其他哲学家现在被封锁
  • 当Emma完成后,她释放了第3和第4分叉,Michel和Karl都跳了起来
  • 当米歇尔完成后,他释放福克斯0和4,朱迪思立即抓住它......然后开始等待;现在没有人关心Fork 4
  • 当Karl完成后,他释放了Fork 2,Gilles立即抓住了
  • 当Gilles完成后,他释放了Fork 1,Judith立即抓住了
  • 当朱迪思完成时,所有5人都吃了

我们观察到非常有限的并发性:Emma首先命中,只有当她完成时才会有两个并行流,一个是Michel,一个是Karl&gt;吉尔斯&gt;朱迪思。

注意:我强迫这个案子暂停了Michel 150ms,然后让他抓住他的第二个叉子。

最后,我们有案例(c):

  • Michel抓住Fork 4,所有其他哲学家现在被封锁
  • 当米歇尔完成后,他释放了分叉4和0,分别由艾玛和朱迪思抓住;朱迪思仍被封锁(先睡觉,然后等待福克斯1),但艾玛开始吃饭
  • 当艾玛完成时......

这里再次没有并发性。

(*)这实际上并没有保证,但150ms是计算机时间长的,除非机器负载很重,否则就会发生。

虽然本书提出的解决方案确实有效(在任何情况下都没有死锁),但它并没有表现出太多的并发性,因此它更像是Rust的展示而不是并发展...但是,它是Rust书而不是并发书!

我不明白为什么Michel的线程首先在围栏上系统安排;但是可以通过让他专门睡觉来轻松应对。

答案 1 :(得分:4)

这是这个例子的半常见问题。程序员倾向于认为线程是“随机的”,因为线程通常具有不同的开始时间和运行长度。大多数线程使用也不会在线程的整个生命周期中锁定共享资源。请记住,线程是确定性的,因为它们是由算法调度的。

在此示例中,主线程创建了一大堆线程,并将它们添加到由操作系统管理的队列中。最终,主线程被调度程序阻塞或中断。调度程序查看线程队列并询问“第一个”线程是否可以运行。如果它是可运行的,则运行一段时间或直到它被阻止。

“第一个”线程取决于操作系统。例如,Linux有多个可调整的调度程序,允许您确定运行哪些线程的优先级。调度程序还可以选择提前或稍后中断线程

如果在线程的最开头添加打印件,则可以看到线程以不同的顺序启动。这是一个基于100次运行的线程首先启动的表:

| Position | Emma Goldman | Gilles Deleuze | Judith Butler | Karl Marx | Michel Foucault |
|----------+--------------+----------------+---------------+-----------+-----------------|
|        1 |            4 |              9 |            81 |         5 |               1 |
|        2 |            5 |             66 |             9 |        17 |               3 |
|        3 |           19 |             14 |             5 |        49 |              13 |
|        4 |           46 |              9 |             3 |        20 |              22 |
|        5 |           26 |              2 |             2 |         9 |              61 |

如果我正确地进行了统计,最常见的起始顺序是:

  1. 朱迪思巴特勒
  2. Gilles Deleuze
  3. Karl Marx
  4. Emma Goldman
  5. Michel Foucault
  6. 请注意,这与代码中定义的哲学家序列相匹配!

    另请注意,算法本身会强制执行排序。除了一位哲学家之外,所有人都先拿起左边的叉子然后再等一下。如果线程按顺序运行,那么每个线程依次在它之前等待。大多数线程都依赖于坐在“左”的线程。如果我们想象一个圆形桌子,每个人都拿着一个左叉(一个死锁),我们选择一个人给一个额外的叉子(打破僵局),那么你可以看到会有一连串的人可以吃。 / p>

    还要记住println!使用标准输出;一个必须受互斥锁保护的可变全局资源。因此,打印可能会导致线程被阻塞并重新安排。

    我在OS X上,这很可能解释了我一直认为与你的不同的顺序。