"Rust语言:安全的并发"

本文原载于2014年3月《程序员》杂志编程语言专题。

Rust 语言是近两年来 Mozilla 正在开发的一种新编程语言,它以并发,安全和实用为口号,主要使用场景是系统编程,用来取代C++语言的角色。有人戏称 Mozilla 由于大量使用 C++,深知其弊端,所以诞生了 Rust。目前,Rust 的两个主要产品是 Mozilla 下一代的浏览器布局引擎 Servo 和 Rust 编译器。由于语言在快速的开发和演进中,每个版本都会产生一些不兼容的变更,所以现在并非是学习这门语言细节并真正使用它的最佳时机。不过,这并不影响我们了解 Rust 语言:作为多年来鲜有的新系统编程语言,他本身有很多新颖之处,也可以帮助我们了解一些编程语言设计的流行趋势。

并发

和现在流行的很多新语言一样,Rust 在语言层面支持了绿色线程(Green threads):Task。Task 作为并发执行的单元,是用户空间的“线程”,创建和调度成本较低,可以大量共存。Task之间通过消息传递通信,没有直接的共享数据。从最近的流行趋势来看,绿色线程几乎已经成为并发方案大战最终的赢家。除了Rust,之前流行的Go语言,Erlang,Python的Gevent,以及最近Clojure世界里正在发展的core.async,采用的都是这种绿色线程的模式。

绿色线程的程序与传统多线程程序的写法几乎一致。在编写服务器程序时,与事件驱动的回调机制相比,编写更简单,表义更清晰。当并发任务增多时,传统的多线程程序由于启动线程和调度线程的成本高而使系统整体性能降低。而绿色线程可以基本不受限制,随意创建。Rust的文档指出在32位系统上可以支持数十万个Task同时存在。

Task也是Rust程序的基础单元,一个Rust进程又多个并行的task组成,main函数本身也是一个Task。Task之间通过一个 (Port, Chan) 元组传递数据。Port和Chan相当于管道的两端,Port用于取数据,Chan用于发送数据。下面的例子里,我们通过do spawn语法(类似Ruby的block语法),启动一个新Task,并打印收到的数据。

fn main(){ 
  let msg = "hello world"; 
  let (port, chan) = Chan::new(); 

  chan.send(msg); 

  do spawn { 
    let received_msg = port.recv(); 
    println(received_msg); 
  } 
} 

引用系统

Rust 语言设计的核心是安全性(这里安全性指safety,而非 security)。Rust 希望通过语言的机制和编译器的功能,把程序员易于犯错,不易检查的问题解决在编译期,避免运行时的Segmentation Fault。Rust 的设计可以说是处处小心。Clojure语言强调可变性给编程带来的复杂性,在 Rust 语言中,设计者对这点也有格外的重视。除非特别声明为 mut,所有 Rust 的局部变量默认都是不可变的,对不可变变量值的修改会导致 编译器直接报错。

Rust 的安全性还通过独有的引用类型系统来实现。

Rust 语言中堆内存块的引用类型叫做box。最新版本的Rust 在语言层面只保留了一种owned box,它在使用时具有一种所有权(ownership)的概念,只有具有所有权的变量才可以访问这段内存。Owned box在同一时刻只允许一个变量作为所有者,它的变量赋值称为move。一旦owned pointer被赋值,用户就无法通过原先的引用访问这块数据,这种错误会在编译时检查。 一个简单的例子:

fn main() {
    let a: ~int = ~50;
    let b: ~int = a;
    println!("{:?}", *a);
}

~ 代表owned box,这里我们把一个包含值为50的owned box赋给owned pointer a。然后把a的所有权通过赋值的形式move给b。最后我们试图通过 *a 访问这个值。在C语言里,这时a和b同时指向统一块内存,可以通过*a访问到这里的值。但Rust的所有权机制给予了这段内存额外的保护。编译这段程序将失败:

own.rs:4:23: 4:24 error: use of moved value: `a`
own.rs:4     println!("{:?}", *a);
                               ^
...
own.rs:3:9: 3:10 note: `a` moved here because it has type `~int`, which is moved by default (use `ref` to override)
own.rs:3     let b: ~int = a;
                 ^

编译器会明确地指出错误的引用在何处被move。事实上所有owned box的生命周期管理都是直接在编译时完成的,编译器通过静态检查跟踪使用情况,完成内存开辟和回收。这是Rust 确保编程正确、安全的重要手段。

新版本的Rust在标准库中提供 std::rc::Rc(引用计数) 和 std::gc::Gc (垃圾回收)类型,取代了原先的managed box,用来提供可以有限共享的引用类型。

在 Task 间传递数据,如果要避免数据拷贝,也有专门的引用类型:用于不可变数据的extra::arc::Arc (atomically reference counted ,原子的引用计数类型) , 以及用于可变数据的 RWArc(带读写锁的原子引用计数类型) 。RWArc在操作可变数据时,通过内在的读写锁控制对共享数据的访问,从而在API层面实现安全性。

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

fn main() {
    let s = ~"hello world";
    let arc_ref = RWArc::new(s);

    for i in range(0, 10) {
        let (port, chan) = Chan::new();
        chan.send(arc_ref.clone());

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

            arc_local_ref.write(|str| {
                str.push_char('!');
            });

            arc_local_ref.read(|str| {
                println(*str);
            });
        }
    }
}

Rust谨慎地定义如此繁多,各具功能的引用类型,就是希望用户在编程过程中,根据应用场景、引用的功能职责,选择合适的类型,进而在引用类型系统和编译器的保护下,减少在运行时出错的机会。这一点也和Clojure的4种引用类型的设计初衷类似,不过Clojure并不能提供太多编译时的安全保护。

更多

篇幅所限,我只选择了Rust最具特点的两个部分介绍。Rust是一门具备自身显著特点,精心设计的语言,而绝非普通的“又一门编程语言”。在语法层面,它包含了模式匹配,闭包,泛型等流行功能,作为系统编程语言,使用的舒适度不亚于脚本语言。另外还可以通过FFI(Foreign Function Interface)调用已有的C语言库,满足了实用性的需要。

如果你也开始对这门新语言感兴趣,可以: