Rust中文翻译8

时间:2022-09-28 14:53:44
我们的第二个工程,来解决一个经典的并发问题.我们称之为"哲学家吃饭问题".这个问题的最初构想是由Dijkstra于1965年提出的,但是这里我们使用1985年Tony Hoare在这篇论文中的版本.
  在古代,一个富有的慈善家给五位杰出的哲学家捐赠了一所学院.每一位哲学家都有一个小房间供他们思考哲学问题;另外有一个公共的房间,放置了一张圆桌,五把椅子,每一把上面写有坐在它上面的哲学家的名字.他们以逆时针方向围绕桌子坐下.每个哲学家的左手边有一把金叉子,中间放着一大碗意大利面,面是可以无限添加满的.哲学家们本来被期望是话尽可能多的时间思考的;但是当他们觉得饿了的时候,他们可以走到餐桌前,坐在他们自己的椅子上,拿起他左边的叉子,然后插入面条中.但是面条有一个令人纠结的性质,就是它必须要再用第二把叉子才能被放入哲学家口中.所以哲学家们就必须拿起他们右边的叉子.当他们吃完的时候他们可以放下手中的所有叉子,然后起身离开继续思考.当然,一把叉子只能同时被一个哲学家使用.如果另一个哲学家想要使用,他必须等待叉子可用时才可以.
这个经典的问题展示了并发问题的几个基本元素.并发问题的原因在于简单的实现方法会产生死锁.例如,让我们看一个简单的算法: 1.哲学家拿起左边的叉子 2.哲学家拿起右边的叉子 3.哲学家吃饭 4.哲学家还回叉子
现在,让我们想象一下事情的发展顺序: 1.哲学家1开始算法,拿起他左边的叉子 2.哲学家2开始算法,拿起他左边的叉子 3.哲学家3开始算法,拿起他左边的叉子
Page 44
4.哲学家4开始算法,拿起他左边的叉子 5.哲学家5开始算法,拿起他左边的叉子 6. ...?所有叉子都被使用了,但是没人可以吃饭!
有很多办法可以解决这个问题.我们会在本文中给出我们的方法.现在,我们先模拟一下这个问题.从哲学家开始:
struct Philosopher {      name: String, }
impl Philosopher {      fn new(name: &str) -> Philosopher {           Philosopher {                name: name.to_string(),           }      } }
fn main() {      let p1 = Philosopher::new("Baruch Spinoza");      let p2 = Philosopher::new("Gilles Deleuze");      let p3 = Philosopher::new("Karl Marx");      let p4 = Philosopher::new("Friedrich Nietzsche");      let p5 = Philosopher::new("Michel Foucault"); } 这里,我们创建了一个结构体来代表一个哲学家.现在,我们仅需要一个名字.我们使用String类型来记录名字,而不是一个&str.通常来说,使用一个拥有自己数据的类型要比使用一个引用要简单. 让我们继续: impl Philosopher {
     fn new(name: &str) -> Philosopher {
          Philosopher {
               name: name.to_string(),
          }
     }
}
这个impl语句块可以让我们在Philosopher结构体上定义一些东西.在这里,我们定义了一个关联放方法new.第一行看起来像这样:
Page 45
fn new(name: &str) -> Philosopher { 我们使用了一个类型是&str,名字叫做name的参数.这是另一个字符串的引用.方法返回了一个Philosopher结构体的实例.
Philosopher {
    name: name.to_string(),
}

它创建了一个新的Philosopher,将它的name设置成我们传入的参数.并不是我们的参数本身,而是调用了参数的to_string()方法.这样就会创建一个&str指向对象的拷贝,然后我们得到一个新的String对象,类型就是name的类型,一个String.
为什么不直接接受一个String类型呢?那样更方便调用啊.如果我们使用String,但是我们的调用者使用了&str,他们就没办法调用我们的方法了.这一灵活性的代价就是我们总要创建一个拷贝.对于这个小的程序,这并不重要,因为我们知道我们总是会使用短的字符串的.
最后一件你需要注意的事是:我们定义了一个哲学家对象,但是看起来什么都没有做.Rust是一个基于表达式的语言,也就是说Rust中的每个东西都是一个表达式,并且会有返回值.方法也同样如此,最后一个表达式会自动返回.因为我们在方法的最后一个表达式创建了一个哲学家对象,我们会返回这个对象作为方法的结束.
new()这个名字对于Rust而言并不意味着什么特殊的事情.但是它是一个创建结构体实例的传统方法名.在我们解释原因之前,我们看一下main()函数: fn main() {      let p1 = Philosopher::new("Baruch Spinoza");      let p2 = Philosopher::new("Gilles Deleuze");      let p3 = Philosopher::new("Karl Marx");      let p4 = Philosopher::new("Friedrich Nietzsche");      let p5 = Philosopher::new("Michel Foucault"); } 我们创建了5个变量绑定,每一个绑定一个哲学家.这是我最喜欢的五个哲学家,你可以替换你自己的.如果我们没有定义那个new()方法的话,我们可以这样做: fn main() {      let p1 = Philosopher{ name: "Baruch Spinoza".to_string()};      let p2 = Philosopher{ name: "Gilles Deleuze".to_string()};      let p3 = Philosopher{ name: "Karl Marx".to_string()};      let p4 = Philosopher{ name: "Friedrich Nietzsche".to_string()};      let p5 = Philosopher{ name: "Michel Foucault".to_string()}; } 这样太繁琐了.使用new()方法还有其他优势,但是在此例中,他就是方便使用而已.
Page 46
现在我们已经有了问题的基础状态,我们可以有很多方法来解决这个问题.我喜欢从结尾开始:让我们设置一个让每一个哲学家都能吃饭的方法.作为一小步,让我们创建一个方法,然后循环让每一个哲学家调用它: struct Philosopher {      name: String, }
impl Philosopher {      fn new(name: &str) -> Philosopher {           Philosopher {                name: name.to_string(),           }      }
     fn eat(&self) {           println!("{} is done eating.", self.name);      } }
fn main() {      let philosophers = vec![           Philosopher::new("Baruch Spinoza");
     Philosopher::new("Gilles Deleuze");
          Philosopher::new("Karl Marx");           Philosopher::new("Friedrich Nietzsche");           Philosopher::new("Michel Foucault");      ];
     for p in &philosophers {           p.eat();      } } 先来看main()方法.与其获得5个独立的哲学家对象变量,我们使用一个哲学家向量Vec<T>来代替他们.Vec<T>也可以叫做"向量",它是一种可以增加大小的类型.当我们使用for循环来遍历一个向量的时候,我们得到向量中每一个成员的一个引用. 在循环体中,我们调用p.eat()方法,它在上面定义过了: fn eat(&self) {     println!("{} is done eating.", self.name); } 在Rust中,方法可以显示指定一个self参数.这就是为什么eat()是一个方法,而new()是一个关联方法:new()没有self参数.我们eat()方法的第一个版本中,只打印出哲学家的名字,然后提到他们要吃完了.运行这个程序会得到如下结果
Page 47
Rust中文翻译8
非常简单,工作完成!我们还没有开始解决这个问题,所以我们还没有完成我们的程序. 下面,我们想让我们的哲学家们不要完成吃饭,而是真正的吃饭.这是下一个版本: use std::thread;
struct Philosopher {
     name: String,
}

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

     fn eat(&self) {
        println!("{} is eating.", self.name);
        thread::sleep_ms(1000);
        println!("{} is done eating.", self.name);
     }
}

fn main() {
     let philosophers = vec![
        Philosopher::new("Baruch Spinoza"),
        Philosopher::new("Gilles Deleuze"),
        Philosopher::new("Karl Marx"),
        Philosopher::new("Friedrich Nietzsche"),
        Philosopher::new("Michel Foucault"),
     ];

     for p in &philosophers {
          p.eat();
     }
}

Page 48
只有一点改变,让我们看看. use std::thread; use语句可以将名字引入命名空间.我们将要从标准库中引入thread模块,所以我们需要使用它. fn eat(&self) {
    println!("{} is eating.", self.name);
    thread::sleep_ms(1000);
    println!("{} is done eating.", self.name);
}

我们现在打印两行信息,中间有一个sleep_ms()语句.这个可以模拟一个哲学家吃饭的时间.如果你运行这个程序,你可以看到每个哲学家轮流吃饭. Rust中文翻译8
漂亮!我们做到了.还有一个问题:我们并没有进行一个并行的操作,而这才是此问题的关键! 为了让哲学家们可以并行吃饭,我们需要一点改变.这是下一次迭代: use std::thread;
struct Philosopher {
     name: String,
}

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

     fn eat(&self) {
        println!("{} is eating.", self.name);
        thread::sleep_ms(1000);
        println!("{} is done eating.", self.name);
     }
}

fn main() {
     let philosophers = vec![
        Philosopher::new("Baruch Spinoza"),
        Philosopher::new("Gilles Deleuze"),
        Philosopher::new("Karl Marx"),
        Philosopher::new("Friedrich Nietzsche"),
        Philosopher::new("Michel Foucault"),
     ];

     let handles: Vec<_> = philosophers.into_iter().map(|p| {
         thread::spawn(move || {
             p.eat();
         })
     }).collect();
    
     for h in handles {
         h.join().unwrap();
     }
}
我们做的所有改变都在main()函数里,然后又增加了第二个代码段!这里是第一个改变: let handles: Vec<_> = philosophers.into_iter().map(|p| {
    thread::spawn(move || {
        p.eat();
    })
}).collect();

Page 50
尽管只有5行代码,确实含金量十足的5行代码.让我们来分析一下. let handles: Vec<_> = 我们引入了一个新的绑定,名叫handles.我们起这个名字是因为我们将要创建一些线程,然后会返回一些handles给每一个线程,通过他们来控制线程的运行.我们需要显式声明类型,原因我们后面再说._是一个占位符.我们的意思是"handles是某种类型的一个容器,你可以指明某种类型是什么东西". philosopher.into_iter().map(|p|{ 我们对philosophers容器调用了into_iter()方法.它会创建一个迭代器,迭代器会获得每一个philosoper的所有权.我们需要把它们传递到每一个线程中.我们对迭代器调用了map方法,它是一个闭包(匿名函数),拥有一个参数,闭包会作用在每一个元素上然后返回. thread::spawn(move || {
    p.eat();
})
这里就是产生并发行为的地方了.thread::spawn方法接受一个闭包作为参数,然后在一个新的线程中执行这个闭包.这个闭包需要一个附加声明,move,来指明这个闭包会接管它捕获的值的所有权.也就是之前的map函数里面的p变量. 在该线程内,我们所做的就只有在p上调用eat()函数. }).collect()(); 最终,我们得到了所有map调用的结果并把他们收集(collect).collect()方法会把它们加入到一个集合中,这就是为什么我们需要声明它的返回类型:我们需要一个Vec<T>类型.thread::spawn函数的返回值就是这个集合的元素,也就是这些线程的handles.Whew! for h in handles {     h.join().unwrap(); } 在main()函数的结尾,我们遍历handles然后调用join()方法,这样会阻塞直到线程执行完毕.这会确保在程序退出之前,所有的线程会先执行完毕.
如果你运行这个程序,你会看到哲学家们没有按顺序吃饭!我们实现了多线程!
Page 51
Rust中文翻译8
Rust中文翻译8