一篇Rust的30分钟介绍

我最近向Rust的文档提交了一个提案。 我通篇提案中一个重要组成部分是对可能据说过Rust的人简短而简单的介绍,以便他们可以肯定Rust是否适合他们。 前几天,我看到了一个精彩的演讲,并认为它能够做为这个介绍的一个很好的基础。 将此视为此类介绍的RFC(Request For Comments)。 很是欢迎反馈在rust-devTwitter上。html

这个教程已经成为官方教程

Rust是一种系统编程语言,专一于强大的编译时正确性保证。 它经过提供很是强大的编译时保证和对内存生命周期的明确控制,改进了其余系统语言(如C ++,D和Cyclone)的思想。 强大的内存保证使编写正确的并发Rust代码比使用其余语言更容易。 这可能听起来很是复杂,但它比听起来更容易! 本教程将让您在大约30分钟内了解Rust。 但愿你至少模糊地熟悉之前的“大括号”语言。 这些概念比语法更重要,因此若是你没有获得每个细节,请不要担忧:本教程能够帮助你解决这个问题。程序员

让咱们来谈谈Rust中最重要的概念:“全部权”,以及它对并发编程(对程序员来说一般是很是困难的任务)的启发。编程

全部权

全部权是Rust的核心,也是其更有趣和独特的功能之一。 “全部权”是指容许哪部分的代码修改内存。 让咱们从查看一些C ++代码开始:安全

int *dangling(void)
{
    int i = 1234;
    return &i;
}

int add_one(void)
{
    int *num = dangling();
    return *num + 1;
}

dangling函数在栈上分配了一个整型,而后保存给一个变量i,最后返回了这个变量i的引用。这里有一个问题:当函数返回时栈内存变成失效。意味着在函数add_one第二行,指针num指向了垃圾值,咱们将没法获得想要的结果。虽然这个一个简单的例子,可是在C++的代码里会常常发生。当堆上的内存使用malloc(或new)分配,而后使用free(或delete)释放时,会出现相似的问题,可是您的代码会尝试使用指向该内存的指针执行某些操做。 更现代的C ++使用RAII和构造函数/析构函数,但它们没法彻底避免“悬空指针”。 这个问题被称为“悬空指针”,而且不可能编写出现“悬空指针”的Rust代码。 咱们试试吧:闭包

fn dangling() -> &int {
    let i = 1234;
    return &i;
}

fn add_one() -> int {
    let num = dangling();
    return *num + 1;
}

当你尝试编译这个程序时,你会获得一个有趣和很是长的错误信息:并发

temp.rs:3:11: 3:13 error: borrowed value does not live long enough
temp.rs:3     return &i;

temp.rs:1:22: 4:1 note: borrowed pointer must be valid for the anonymous lifetime #1 defined on the block at 1:22...
temp.rs:1 fn dangling() -> &int {
temp.rs:2     let i = 1234;
temp.rs:3     return &i;
temp.rs:4 }

temp.rs:1:22: 4:1 note: ...but borrowed value is only valid for the block at 1:22
temp.rs:1 fn dangling() -> &int {      
temp.rs:2     let i = 1234;            
temp.rs:3     return &i;               
temp.rs:4  }                            
error: aborting due to previous error

为了彻底理解这个错误信息,咱们须要谈谈“拥有”某些东西意味着什么。 因此如今,让咱们接受Rust不容许咱们用悬空指针编写代码,一旦咱们理解了全部权,咱们就会回来看这块代码。编程语言

让咱们先放下编程一下子,先聊聊书籍。 我喜欢读实体书,有时候我真的很喜欢一本书,并告诉个人朋友他们应该阅读它。 当我读个人书时,我拥有它:这本书是我所拥有的。 当我把书借给别人一段时间,他们向我“借用”这本书。 当你借用一本书时,在特定的一段时间它是属于你的,而后你把它还给我,我又拥有它了。 对吗?函数

这个概念也直接应用于Rust代码:一些代码“拥有”一个指向内存的特定指针。 它是该指针的惟一全部者。 它还能够暂时将该内存借给其余代码:代码“借用”它。 借用它一段时间,称为“生命周期”。工具

这是关于全部权的全部。 那彷佛并不那么难,对吧? 让咱们回到那条错误信息:error: borrowed value does not live long enough。 咱们试图使用Rust的借用指针&,借出一个特定的变量i。 但Rust知道函数返回后该变量无效,所以它告诉咱们:性能

borrowed pointer must be valid for the anonymous lifetime #1

... but borrowed value is only valid for the block。

优美!

这是栈内存的一个很好的例子,但堆内存呢? Rust有第二种指针,一个'惟一'指针,你能够用〜建立。 看看这个:

fn dangling() -> ~int {
    let i = ~1234;
    return i;
}

fn add_one() -> int {
    let num = dangling();
    return *num + 1;
}

此代码将成功编译。 请注意,咱们使用指针指向该值而不是将1234分配给栈:~1234。 你能够大体比较这两行:

// rust
let i = ~1234;

// C++
int *i = new int;
*i = 1234;

Rust可以推断出类型的大小,而后分配正确的内存大小并将其设置为您要求的值。 这意味着没法分配未初始化的内存:Rust没有null的概念。万岁! Rust和C ++之间还有另一个区别:Rust编译器还计算了i的生命周期,而后在它无效后插入相应的free调用,就像C ++中的析构函数同样。 您能够得到手动分配堆内存的全部好处,而无需本身完成全部工做。 此外,全部这些检查都是在编译时完成的,所以没有运行时开销。 若是你编写了正确的C ++代码,你将编写出与C++代码基本上相同的Rust代码。并且因为编译器的帮忙,编写错误的代码版本是不可能的。

你已经看到了一种状况,全部权和生命周期有利于防止在不太严格的语言中一般会出现的危险代码。如今让咱们谈谈另外一种状况:并发。

并发

并发是当前软件世界中一个使人难以置信的热门话题。 对于计算机科学家来讲,它一直是一个有趣的研究领域,但随着互联网的使用爆炸式增加,人们正在寻求改善给定的服务能够处理的用户数量。 并发是实现这一目标的一种方式。 但并发代码有一个很大的缺点:它很难推理,由于它是非肯定性的。 编写好的并发代码有几种不一样的方法,但让咱们来谈谈Rust的全部权和生命周期的概念如何帮助实现正确而且并发的代码。

首先,让咱们回顾一下Rust中的简单并发示例。 Rust容许你启动task,这是轻量级的“绿色”线程。 这些任务没有任何共享内存,所以,咱们使用“通道”在task之间进行通讯。 像这样:

fn main() {
    let numbers = [1,2,3];

    let (port, chan)  = Chan::new();
    chan.send(numbers);

    do spawn {
        let numbers = port.recv();
        println!("{:d}", numbers[0]);
    }
}

在这个例子中,咱们建立了一个数字的vector。 而后咱们建立一个新的Chan,这是Rust实现通道的包名。 这将返回通道的两个不一样端:通道(channel)和端口(port)。 您将数据发送到通道端(channel),它从端口端(port)读出。 spawn函数能够启动一个task。 正如你在代码中看到的那样,咱们在task中调用port.recv(),咱们在外面调用chan.send(),传入vector。 而后打印vector的第一个元素。

这样作是由于Rust在经过channel发送时copy了vector。 这样,若是它是可变的,就不会有竞争条件。 可是,若是咱们正在启动不少task,或者咱们的数据很是庞大,那么为每一个任务都copy副本会使咱们的内存使用量膨胀而没有任何实际好处。

引入Arc。 Arc表明“原子引用计数”,它是一种在多个task之间共享不可变数据的方法。 这是一些代码:

extern mod extra;
use extra::arc::Arc;

fn main() {
    let numbers = [1,2,3];

    let numbers_arc = Arc::new(numbers);

    for num in range(0, 3) {
        let (port, chan)  = Chan::new();
        chan.send(numbers_arc.clone());

        do spawn {
            let local_arc = port.recv();
            let task_numbers = local_arc.get();
            println!("{:d}", task_numbers[num]);
        }
    }
}

这与咱们以前的代码很是类似,除了如今咱们循环三次,启动三个task,并在它们之间发送一个Arc。 Arc :: new建立一个新的Arc,.clone()返回Arc的新的引用,而.get()从Arc中获取该值。 所以,咱们为每一个task建立一个新的引用,将该引用发送到通道,而后使用引用打印出一个数字。 如今咱们不copy vector。

Arcs很是适合不可变数据,但可变数据呢? 共享可变状态是并发程序的祸根。 您可使用互斥锁(mutex)来保护共享的可变状态,可是若是您忘记获取互斥锁(mutex),则可能会发生错误。

Rust为共享可变状态提供了一个工具:RWArc。 Arc的这个变种容许Arc的内容发生变异。 看看这个:

extern mod extra;
use extra::arc::RWArc;

fn main() {
    let numbers = [1,2,3];

    let numbers_arc = RWArc::new(numbers);

    for num in range(0, 3) {
        let (port, chan)  = Chan::new();
        chan.send(numbers_arc.clone());

        do spawn {
            let local_arc = port.recv();

            local_arc.write(|nums| {
                nums[num] += 1
            });

            local_arc.read(|nums| {
                println!("{:d}", nums[num]);
            })
        }
    }
}

咱们如今使用RWArc包来获取读/写Arc。 RWArc的API与Arc略有不一样:读和写容许您读取和写入数据。 它们都将闭包做为参数,而且在写入的状况下,RWArc将获取互斥锁,而后将数据传递给此闭包。 闭包完成后,互斥锁被释放。

你能够看到在不记得获取锁的状况下是不可能改变状态的。 咱们得到了共享可变状态的便利,同时保持不容许共享可变状态的安全性。

但等等,这怎么可能? 咱们不能同时容许和禁止可变状态。 是什么赋予了这种能力的?

unsafe

所以,Rust语言不容许共享可变状态,但我刚刚向您展现了一些容许共享可变状态的代码。 这怎么可能? 答案:unsafe

你看,虽然Rust编译器很是聪明,而且能够避免你一般犯的错误,但它不是人工智能。 由于咱们比编译器更聪明,有时候,咱们须要克服这种安全行为。 为此,Rust有一个unsafe关键字。 在一个unsafe的代码块里,Rust关闭了许多安全检查。 若是您的程序出现问题,您只须要审核您在不安全范围内所作的事情,而不是整个程序。

若是Rust的主要目标之一是安全,为何要关闭安全? 嗯,实际上只有三个主要缘由:与外部代码链接,例如将FFI写入C库,性能(在某些状况下),以及围绕一般不安全的操做提供安全抽象。 咱们的Arcs是最后一个目的的一个例子。 咱们能够安全地分发对Arc的多个引用,由于咱们确信数据是不可变的,所以能够安全地共享。 咱们能够分发对RWArc的多个引用,由于咱们知道咱们已经将数据包装在互斥锁中,所以能够安全地共享。 但Rust编译器没法知道咱们已经作出了这些选择,因此在Arcs的实现中,咱们使用不安全的块来作(一般)危险的事情。 可是咱们暴露了一个安全的接口,这意味着Arcs不可能被错误地使用。

这就是Rust的类型系统如何让你不会犯一些使并发编程变得困难的错误,同时也能得到像C ++等语言同样的效率。

总而言之,伙计们

我但愿这个对Rust的尝试能让您了解Rust是否适合您。 若是这是真的,我建议您查看完整的教程,以便对Rust的语法和概念进行全面,深刻的探索。

相关文章
相关标签/搜索