今天(2019-11-07)Rust终于发布了期待已久的v1.39版本,增加了重量级的async/await关键字支持。Rust作为一个2015年才发布正式版的新星,使用人数寥寥,却能在StackOverflow发起的"最喜爱的编程语言“年度投票中连续四年蝉联第一。Rust凭什么能够击败Python等众多语言连续四年制霸?这一切的背后有着什么样的秘密?是人性的扭曲还是道德的沦丧? 冲着async/await支持,笔者最近特意在一个小项目中试用了Rust v1.39。虽然只是个小工程,写完后深刻体会都了Rust无与伦比的优势。
几十年以来,C/C++一直都是操作系统内核、浏览器、NGINX、Redis等这类性能关键(Performance Critical)软件开发的首选语言。但存在的问题也非常明显:空指针、野指针、内存越界。轻则Segment Fault崩溃,重则导致难以发现的0-day漏洞。
寄希望于开发人员不犯一丁点错误写出内存安全的软件无异于痴人说梦。因此自上世纪90年代开始,以Java为代表的一系列自动化内存管理的编程语言兴起。通过在性能方面做出适当的妥协和让步,引入GC等机制,来实现内存安全。
从此,软件开发常常面临这样的两难选择:要么为了安全性而在性能上做出妥协;要么为了极致的性能牺牲安全性。例如在使用Java开发某网络服务时,为了实现zero-copy转发,JVM管理的byte[]已经无法满足要求,需要使用不受GC管理的Direct Buffer。比如采用netty.io中引用计数管理的ByteBuf ,使用时必须慎之又慎,否则非常容易导致内存泄漏或者内存提前释放,特别是加上逻辑分支异常处理的情况下这些错误难免出现。从那时起,在性能与安全性的选择上似乎就变成了“鱼与熊掌不可兼得”的难题。
而Rust在设计之初就设定了必须同时兼顾性能和安全性的目标——既要有"底层语言"一样的性能,又要有"高层语言"的安全性(甚至在线程安全性方面比Java/C#等语言更加安全!)。这样一种决不妥协的设计哲学,让Rust从一出生开始就注定了它的不平凡。
Rust没有传统的垃圾回收器。为了解决内存安全性,Rust独创了所有权(Onwership)系统与租借检查器(Borrow Checker)。
比如下面的代码 s 是一个referent,它指向内存中的一个字符串对象。编译器编译代码时候,会在referent的作用域范围结束位置自动生成代码销毁所指的内存对象。即referent拥有着此内存对象的所有权。所有权也能够从一个referent转移到另一个referent,亦即所有权转移(move)。无论怎么转移,对于内存中的某个对象在同一时间内有且仅有一个referent获得了它的所有权。听上去referent有点像C++中的auto_ptr智能指针,但Rust的referent更加细腻和强大。 b1/b2/b3看上去很像指针,在Rust中它们被称为租借(borrow)。
其中b1是可变(mutable)租借,而b2/b3属于不可变(immutable)租借。在同一时间内,一个referent可以有多个不可变租借或者一个可变租借,这点类似于读写锁(Readers-Writer lock);租借的作用域不能超过其referent作用域,以此防止野指针的出现。referent和borrow都没有空值,因此杜绝了空指针。多个对象之间的borrow不能出现闭环(相互引用或者循环引用),极大地避免隐式内存泄漏。关于referent和borrow的详细规则可以见下图(点击图片看大图)。 所有的这些规则都由编译器实现。简而言之,Rust通过编译期的规则实现了没有GC下的内存安全。
在内存安全的基础上,Rust更进一步地保证了线程安全性。
默认情况下一个对象只能被拥有所有权的线程访问。实现了Send trait的数据类型所有权能够在多个线程间转移,但线程间不存在共享数据。实现了Sync trait的数据类型能够被多个线程同时访问,代表着该数据类型实现了线程间的同步操作。需要说明的是,Send和Sync都属于编译器自动添加的标记(marker trait),并不需要开发人员实现。如果某个数据类型所有成员都是Send或Sync的,那么该数据类型也会被自动标记为Send或Sync。这样就自然而然地保证了线程安全性。
benchmark显示Rust有着媲美C/C++的速度,就执行速度而言狂虐Golang/Java。
在TechEmpower进行的主流Web框架性能测试中,由微软的Kim使用Rust构建的actix开源框架在多项测试场景中拔得头筹。 依据历年的数据,70%的安全漏洞都和内存安全有关。Rust完美解决了安全性和性能的两难问题,也让它成为了开发底层应用的更优选择。目前,Intel、微软和Linux都在积极准备使用Rust开发驱动程序甚至操作系统内核以改善日益严峻的安全问题。
Rust在设计之初被定位为多范式语言(multi-paradigm language),在汲取了其它语言大量成功经验基础上,并不限定用户采用某种特定的范式编码。
Rust支持传统的命令式风格(imperative style)代码无须赘述。Rust同时也吸取了大量ML阵营编程语言的诸多特性,比如模式匹配、不可变量与无副作用方法、Haskell的类型系统、反向类型推导和trait特性系统等。这使Rust具备高度抽象表达能力的同时,又能获得极致的性能和对底层细腻的掌控能力。
使用Rust进行多线程应用开发,也有极高的自由度。
可以选择线程间共享数据,通过传统的操作系统原语同步或者lockless数据结构;也可以选择CSP(Communicating sequential processes)模型,就如Golang开发手册中总结的那样“Do not communicate by sharing memory; instead, share memory by communicating.” – 通过同步channel将多个线程组合起来而避免数据共享。还可以选择如Erlang那样的异步Actor模型对于高并发I/O应用的开放,异步编程是首选项。传统的异步依赖于回调或者闭包,实现起来麻烦不说,代码可读性也差强人意。而Rust这次新添加的两个关键字类似于C#和Python中的async / await ,用户无需再依赖于回调或者闭包,用最自然的方式编写可读性佳的I/O操作代码,代码看上去和同步代码无异,而编译器将异步调用部分编译为状态机从而实现异步执行。这点感觉非常好。
Rust学习曲线略为陡峭,突破新手期所需时间数倍于Golang/Python,如果有着C++和Haskell经验对这一阶段帮助很大。突破新手期后就一片坦途,不再需要编写C/C++程序那样的谨小慎微,而async/await关键字的引入大大方便了异步编程,很多时候感觉都在用C#/Java这样的高层语言编程–关注于业务和程序结构而不用去担心某个对象错误的释放。
需要吐槽的是各自为战的生态系统,为了熟练使用async/await关键字,关于future就需要知晓std::future::Future 和futures::future::Future, 而后者还分为0.1版本和0.3版本。又由于不同的库依赖于不同的future实现,所以还需要精通它们之间各种转换╮(﹀_﹀)╭,刚开始时真有点头大。
瑕不掩瑜。Rust可以说是目前最全面的编程语言。它既可以用于开发与硬件打交道的底层,也能用来开发对性能有极致要求的微服务,甚至还可以开发运行在浏览器中的WebAssembly网站(Seed代码示例如下)。在拥有极致性能的同时又不失鲁棒性和安全性。编程风格自由且高效,能够被广泛的其它语言开发者(C++/Haskell/Python/Scala等)接纳。这些优势决定了Rust将会大放异彩!
fn view(model: &Model) -> impl View<Msg> { let plural = if model.count == 1 {""} else {"s"}; // Attrs, Style, Events, and children may be defined separately. let outer_style = style!{ St::Display => "flex"; St::FlexDirection => "column"; St::TextAlign => "center" }; div![ outer_style, h1![ "The Grand Total" ], div![ style!{ // Example of conditional logic in a style. St::Color => if model.count > 4 {"purple"} else {"gray"}; St::Border => "2px solid #004422"; St::Padding => unit!(20, px); }, // We can use normal Rust code and comments in the view. h3![ format!("{} {}{} so far", model.count, model.what_we_count, plural) ], button![ simple_ev(Ev::Click, Msg::Increment), "+" ], button![ simple_ev(Ev::Click, Msg::Decrement), "-" ], // Optionally-displaying an element if model.count >= 10 { h2![ style!{St::Padding => px(50)}, "Nice!" ] } else { empty![] } ], success_level(model.count), // Incorporating a separate component h3![ "What are we counting?" ], input![ attrs!{At::Value => model.what_we_count}, input_ev(Ev::Input, Msg::ChangeWWC) ] ] } #[wasm_bindgen(start)] pub fn render() { seed::App::build(|_, _| Init::new(Model::default()), update, view) .build_and_start(); }