导读:Rust 是一门重视功能和安全的体系编程言语,经过其共同的一切权体系、借用体系和类型体系,成功地处理了传统体系编程中的许多难题。其开发者友好的语法、丰厚的标准库和强壮的社区支撑,使得 Rust 成为当今编程范畴中备受重视的言语之一。

01 引言

Rust 现已不算是一门年青的言语了,其诞生时间跟 Go 言语差不多。2006 年 Rust 作为 Graydon Hoare 的个人项目出现,2007 年 Google 开端规划 Go。但很明显,Go 的开展要好得多。Rust 在 2015 年才发布了 1.0 版别,而 Go 在 2016 年现已成为了 TIOBE 的年度言语。相较而言 Rust 的开展和远景似乎不怎么好,但其实这与 Rust 言语的定位有十分大的关系。Rust 开始是作为一种在体系编程范畴里代替 C/C 而出现的言语,其开展自然要缓慢许多。因为在体系编程范畴,每走一步都要求十分扎实。

我对 Rust 形象比较深入的有两件工作:首先是看到一篇文章称其学习曲线十分峻峭,其时就比较好奇一门言语能够难到什么程度。其次则是因为 Linus Torvalds 决议在 Linux Kernel 里添加对 Rust 的支撑。Linus 以严苛知名,能遭到 Linus 的青睐肯定不是一件简略的工作,这说明 Rust 这门言语必定有独到之处。

最近几年,微软、AWS 等大型商业公司逐步开端运用 Rust 来编写或重写重要体系。开源界许多重视安全因素的组件,如 sudo/su 也在运用 Rust 进行重写。Rust 除了在体系编程范畴变得流行起来,也是 WASM 范畴里的引荐言语。这一方面说明 Rust 言语现已逐步老练,另一方面也说明晰 Rust 有十分强的表现才能,在各个范畴都能胜任。所以 Fabarta 在开发多模态智能引擎 ArcNerual 时,经过多方面的权衡后挑选了 Rust 言语。

在曩昔的一年多的时间里,团队从 0 到 1 开端学习并运用 Rust,在开发功率、安全性、并发、异步编程等范畴都有深化的实践,咱们成功发布了 ArcNerual 多模态智能引擎(ArcNerual 详情能够参见附录 1、附录 2)。回过头来看,这是一个十分成功的决议。团队成员之前大多具备 C/C 的布景,在上手速度上会有一些优势,后期也有一些 Java/前端布景的同学介入,在其他团队成员的协助下,上手时间均不超过三个月,实践下来并不像网上一些文章写的那样有十分峻峭的学习曲线。

本文旨在扼要叙述 Rust 言语的特性、优势以及实际生产项目中需求的一些注意事项,以便协助您评价现在团队布景、合适范畴、上手本钱、熟悉周期、收益等各个方面的内容,终究能够合理决策是否要在新的项目中引进 Rust 言语。

02 Rust 言语概述

Rust 的开展阅历了多个阶段,从开始的试验性质到逐步老练为一门体系级编程言语。Rust 开始仅仅 Graydon Hoare 的个人项目,他那时是 Mozilla 的雇员。所以后来 Mozilla 公司开端支撑这个项目,并于 2010 年 5 月初次揭露发布。在其前期阶段,Rust 首要是作为 Mozilla 的研讨项目,致力于处理在编写浏览器引擎(如 Firefox 的 Gecko 引擎)时所面对的内存安全和并发性等挑战。

后来,其共同的一切权体系和内存安全性引起了业界的广泛重视。2010 年之后,Rust 的开发逐步成为一个开源社区驱动的进程,而不再仅仅是 Mozilla 的内部项目。跟着社区的不断壮大和创始人的离去,Rust 逐步超越了单一公司的项目,成为一个敞开、多元的编程言语社区,并不受个别核心人物所操控。

Rust 是一种体系级编程言语,其规划方针能够首要概括为以下三个方面:

  • 安全性(Safety): Rust 的最重要规划方针之一便是供给高水平的内存安全性与并发安全性。 经过一切权体系、借用查看器和生命周期机制,Rust 在编译时能够防止许多常见的内存过错,如空指针引证和缓冲区溢出。这使得开发者能够编写愈加健壮、可靠的代码,减少了许多传统体系级编程言语中简略出现的安全漏洞,当然从某些方面来说,安全性确保引进是 Rust 难以学习的首要原因,但只要熟悉并运用妥当,这会是一个极大的优势,实际上 C 也开端评论引进内存安全机制。

  • 高生产力(Productive)“能编译就能工作”。 C/C 的程序员会清楚的知道编译经过仅仅很小的一步,后续还有十分经典的相似“Segmentation fault”过错等着去排查与处理,但 Rust 在这方面给了开发者十分强的决心,叠加零本钱笼统原则,开发者能够放心的将内存、并发等问题交给 Rust ,然后极大的提升开发功率。

  • 实用性(Practicality): Rust 的规划旨在成为一门实用的体系级编程言语,“you can do anything with Rust”。 Rust 不只具有对底层硬件的直接操控才能,一起答应开发者运用高档笼统来表达杂乱概念。它在功能和开发者友好性之间找到平衡,使得开发者能够编写高功能的代码,一起又不牺牲开发者的便利性。

受益于 LLVM(Low Level Virtual Machine) 这样的编译器基础设施,Rust 在这些规划方针的指导下完成了现代言语的一切特征:

  • 静态类型体系:在编译时查看类型,这有助于提前捕获过错,进步代码的稳定性和可维护性。Rust 有一套强壮的类型体系来协助咱们正确地编写代码。

  • 主动内存办理:减轻开发者手动办理内存的负担。现代言语一般经过废物收回(garbage collection)机制来完成,Rust 运用了共同的一切权与借用查看体系,在功能上更有优势。

  • 模块化规划:模块化对软件工程的重要性不言而喻,在此基础之上,Rust 更进一步供给了先进的包办理与构建东西 Cargo,能够十分便利的办理模块及依靠。

  • 丰厚的编程范式:无论是进程式仍是面向方针编程和函数式编程,Rust 均供给了强壮的支撑,这使得开发者能够挑选合适特定问题的编程风格。

  • 异常处理:站在巨人的膀子上,供给一致、高效的机制来处理异常,使得开发者能够更简略地辨认和处理运行时过错。

  • 多线程和并发支撑、跨渠道性。

除此之外,Rust 有一些共同或值得着重的特征:

  • 一切权体系:共同的一切权体系确保内存办理的安全性,防止了悬垂指针和内存泄漏等问题。

  • 借用和生命周期:引进借用和生命周期的概念,使得在不移交一切权的情况下安全地拜访和修正数据。

  • 零本钱笼统:答应开发者运用高层次的笼统,但不引进额定的运行时开支,坚持了体系编程言语的高功能特性。

  • 形式匹配:强壮的形式匹配语法使得开发者能够高雅地处理各种杂乱的数据结构和状态。

  • 异步编程支撑:async/await 语法和 Future trait 的引进使得异步编程愈加易读和高效。

  • 元编程:共同而强壮的宏编程体系。

  • 友好的 FFI(Foreign Function Interface):答应 Rust 与其他编程言语特别是 C 言语进行交互。然后完成跨言语的协同开发与运用现有财物。

在本文中,咱们首要经过一切权体系、借用和生命周期、零本钱笼统这几个特性来深化观察一下 Rust 言语, 其他的一些特功能够重视本专栏后续的专题解读文章。

03 Rust 言语特性

一切权体系

一切权体系能够说是 Rust 中最为共同和核心的功能了,它旨在处理内存安全性和并发性问题。正是一切权概念和相关东西的引进,Rust 才能够在没有废物收回机制的前提下保障内存安全。一起,这也是大家以为 Rust 难学的根本原因,因为它对整个言语产生了影响,让 Rust 看起来和其他言语十分不相同。

一切权体系自身并不杂乱,简略的说,便是在 Rust 中,Rust 中的每一个值都有一个对应的变量作为它的一切者,在同一时间内,值有且仅有一个一切者,举一个简略的比方:

let s1 = String::from("hello, world!");
let s2 = s1;
println!("{}, world!", s1);  // 编译犯错, s1 不再可用了。

假如上面这个简略的比方让你有别致感,这就说明晰 Rust 言语的不同。一切权体系的引进,让 Rust 默许运用一种叫做“移动”的语义。在上面这个比方中,s1 被移动到了 s2,要了解移动及其本钱,你需求了解一个变量所占用的内存什么时分在栈上分配、什么时分在堆上分配以及他们在内存中的大致布局。这与具有废物收回机制的言语是不同的,也是体系编程言语里需求时间重视的概念。

毫无疑问,一切权体系的引进添加了编程时的杂乱度与一些内存拷贝操作(相关于 C/C 来说),但这个本钱是必要的。一切权体系界说了一套规矩,界说了在程序履行进程中如何传递、借用和释放这些值。关键的概念包括“借用”、“生命周期”。接下来,咱们来进一步看看。

借用和生命周期

借用和引证

不是一切的当地都需求移动一切权,借用和引证正是为了在不移动一切权的情况下拜访值。借用能够是不行变的(&T)或可变的(&mut T),简略推导出一个变量能够有多个不行变借用,但只答应一起有一个可变的借用:

  • 不行变借用:运用 &T 语法,答应多个当地一起引证相同的值,但这些引证是只读的,不能修正原始值。这有助于防止数据竞赛和并发问题。
fn main() {
    let original = 42;
    let reference = &original;  // 不行变借用
    println!("The original value is: {}", original);
    println!("The reference value is: {}", reference);
}
  • 可变借用:运用 &mut T 语法,答应代码获取对值的可变引证,但在同一时间只能有一个可变引证存在。这防止了数据竞赛,确保在任何时分只要一个当地能够修正值。
fn main() {
    let mut original = 42;
    let reference = &mut original;  // 可变借用
    *reference  = 10;  // 修正原始值
    println!("The modified value is: {}", original);
}

生命周期

生命周期是 Rust 用来办理引证有用性的一种机制。它们是一系列标示,用于指示引证在代码中的有用范围,以便编译器在编译时查看引证的合法性。简略的说 Rust 会在产生借用的当地分配一个生命周期,然后进行引证有用性的查看。前期的 Rust 运用一个十分简略的依据词法的办法分配生命周期,有许多当地依靠开发者进行标示,给开发者构成了较大的约束与负担。但跟着 RFC-2094-nll(Non Lexical Lifetimes)的优化,Rust 编译器的推导才能越来越强,需求开发者标示的当地现已大大减少。

生命周期运用单引号来表明,比方下面的函数中,Rust 自身没有办法知道 s1 和 s2 的生命周期,需求依靠开发者指出,其间 ‘a 即为生命周期的标示:

fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

生命周期的引进使得 Rust 能够在不放弃内存安全性的一起,答应灵活的数据引证和传递。一起编译器能够在编译时进行查看,确保代码的正确性,是借用查看的暗地功臣。针对生命周期引进的一些约束及学习本钱, Rust 成立了 Polonius 工作组来对借用查看进行进一步优化,相信很快开发者不再需重视这个概念,让生命周期真正退居暗地。

零本钱笼统

零本钱笼统是 Rust 中的一项基本原则,体现在言语规划的各个方面。零本钱笼统是指在高档别表达代码概念时,底层生成的机器码能够坚持高效履行,而不引进运行时功能开支。这一特性使得开发者能够以更笼统、更通用的方法编写代码,而不用忧虑笼统带来的功能损耗,“在编译时能处理的,不留到运行时”

这体现在十分多的方面:

  • 比方经过一切权体系处理主动内存办理,而不是引进废物收回。

  • Rust 中的泛型(Generics)答应开发者编写适用于不同数据类型的通用代码。经过在编译时实例化泛型代码,Rust 能够生成与手动编写特定类型代码相媲美的功能,一起确保代码的通用性和灵活性。

  • 引进了 trait 的概念,使得开发者能够界说笼统接口。经过 trait,能够在不同类型上完成相同的行为,而不会引进额定的运行时开支。这种方法答应代码更好地习惯多态性,一起坚持高功能。

  • Rust 强壮的宏体系(Macros)答应开发者编写形式匹配和代码生成的通用模板,这些宏在编译时打开,生成实际的代码。经过宏,能够在代码中引进更高层次的笼统,而不会导致功能损耗。

这些零本钱笼统的机制与原则,使得 Rust 成为一门一起寻求功能和笼统的现代编程言语,在确保开发功率的一起不损失功能。

04 Fabarta 对 Rust 的运用

在完成 ArcNerual 的时分,咱们的开发言语选型首要考虑几个方面:

  • 不希望手动办理内存以提升开发功率以及防止由内存引起的安全性及过错。

  • 因为是底层数据库体系,所以也不想引进废物收回器而构成的不行预见的体系卡顿。

  • 有活泼的社区与丰厚高质量的三方库。

  • 便利运用巨大的其他言语特别是 C/C 财物。

  • 在满意上述条件的情况下,尽或许的高功能。

从以上这几个方面出发,咱们没有费特别多的时间就选定了 Rust。挑选一门新的言语,在最开端写代码的时分,总是会写成自己最熟悉的那门言语。以咱们团队为例,团队成员大多具有 C/C 布景,所以最开端的代码便是 C/C 风格,然后跟着不断的熟悉与深化,开端渐渐构成一些最佳实践。一些方案会依据编写的体系、团队的偏好、体系的压力而有所不同。从 Fabarta 的实践来看,咱们以为以下几个问题在开始就需求特别注意。

编程范式 / 规划形式的挑选

在许多言语中这都不是一个问题,或许言语自身现已帮你做出了挑选。比方 C /Java 中,一般都会挑选面向方针的形式。可是 Rust 中稍有不同,他并没有供给一种首要的编程范式。你能够依据项目的需求和编写代码的上下文挑选不同的范式或形式,以充分发挥 Rust 的灵活性和表达力。Rust 支撑多种编程范式,包括面向方针、函数式、进程式等,一起还具有强壮的形式匹配和零本钱笼统的才能,而不用拘泥于其间的一种,这会让你的代码愈加简洁高效。

比方在面向方针编程中,咱们常常运用承继来完成多态。但 Rust 并不支撑承继,而是运用 trait 来供给动态分发机制。比方在 ArcNerual 中咱们会供给许多数据库算子,这些算子具有相同的行为,咱们将这些相同的行为笼统为 trait:

pub trait Processor {
    fn do_process(&self) {
        ...
    }
} 

不同的算子会依靠不同的数据:

struct VertexScan {
    ...
}
struct Filter {
    ...
}

但它们有相同的行为:

impl Processor for VertexScan {
    fn do_process(&self) {
        ...
    }
}
impl Processor for Filter {
    fn do_process(&self) {
        ...
    }
}

咱们能够运用动态分发形式来完成多态:

fn run_processor(op: &dyn Processor) {
    op.do_process();
    ...
}

这一方面算是一种方针编程的模仿,动态分发相对来说会有一些运行时开支,编译期的一些优化比方内联优化无法在动态分发的代码中完成,可是经过 Rust 的形式匹配,完全能够将上述行为转为静态分发,在上面的比方中,咱们事先知道有哪些算子,所以能够运用 Rust 强壮的 enum:

pub enum AnyProcessor {
    VertexScan(VertexScan),
    Filter(Filter),
    ...
}
impl Processor for AnyProcessor {
    fn do_process(&self) {
        match self {
            AnyProcessor::VertexScan(vertex_scan) => vertex_scan.do_process(),
            AnyProcessor::Filter(filter) => filter.do_process(),
            ...
        }
    }
}

而这些添加的代码能够经过宏的方法轻松去除。在 ArcNerual 的完成进程中,咱们充分运用了 Rust 多范式的灵活性来降低代码的杂乱度与提升功能。业界在不同的范畴也沉淀出了不同的形式,比方游戏编程里提出了一种 ECS (Entity-Component-System)形式,团队在运用 Rust 之初,能够多思考这个问题,结合 Rust 的多范式编程,会对咱们处理问题有十分大的协助。

并发形式的挑选

出于以下几个原因的考虑,与编程范式相同,Rust 支撑多种并发形式并把挑选权也交给了开发者:

  • Rust 需求有操控底层的才能,所以要供给和 OS 进程/线程相对应的概念。

  • Rust 不能有一个特别大的运行时,所以没有像 Go 相同供给协程(前期曾经有过 green thread,现已去除)。

  • 着重零本钱笼统,不需求为用不到的才能付费。

所以你需求据自己的需求挑选相应的并发模型。假如是核算密集型程序,那简略地运用操作体系多线程模型就足够了;假如是 IO 密集型,或许事件驱动模型就能满意需求。

在 Rust 生态体系中,异步编程变得越来越遍及,特别是在处理网络和大规模并发的运用程序中。关于 ArcNerual 来说,有很多的 IO 操作,也有很多的核算操作,所以咱们挑选了异步形式并运用了 tokio 运行时,这儿的关键是要尽早做出挑选。尽管能够在运用异步模型时,一起运用其他并发模型,但强烈建议不要这么做或许考虑清楚后再决议。因为在本质上,同步和异步是两套不兼容的并发模型。异步中引进同步代码是一个十分大的风险,或许会引起各种意料之外的状况。在选定异步形式后,关于运用到的锁、通讯等依靠包也需求审慎引进。在 ArcNerual 的开发进程中,咱们就遇到过因为引进不适用于异步编程的库而引起程序阻塞。关于异步编程与 tokio 方面的实践能够参见文末附录 3。

safe/unsafe 的运用

在开始 Rust 的借用查看器或许会让你感到莫名的阻力,Rust 社区也常常有这方面的评论,比方在《Rust Koans》(详见附录 4)这篇文章中就生动的描绘了这种感觉。当 C/C 程序员发现有 unsafe Rust 的时分或许会眼前一亮,终于回到了熟悉的范畴,可是,团队有必要明确在什么场景下能够用 unsafe Rust,依据咱们实践的经历,初期仅建议在调用外部代码的时分运用,后期能够在功能优化的时分有针对性的进行引进,需求及早和团队明确,防止 unsafe 代码失控。

内存分配

曩昔体系级编程言语中一向倾向于由程序员来办理内存,这首要是依据功能方面的考量,因为一次内存分配操作相对来说是比较耗时的。有一些言语比方 Zig,着重没有隐式内存分配,每一个内存分配操作均需求程序员来操作并带入自界说内存分配器。Rust 作为一门以主动内存办理为重要特征的体系级编程言语,其实是归纳了几种言语的特性,现在在内存分配方面全体显得比较杂乱而杂乱,这也是 Rust 社区现在面对的一个重要课题。Rust 开始运用的内存分配器是 Jemalloc,后来因为 Jemalloc 的多渠道支撑问题、Rust 程序嵌入问题等原因,切换为各个体系的默许分配器。

现在 Rust 会运用体系的默许分配器,在 Linux 上便是 ptmalloc2。相对来说其功能不如 tcmalloc/jemalloc,Rust 供给了相关接口,能够经过代码进行更改,比方在 ArcNerual 中咱们经过如下代码将大局内存分配器设置为 Jemalloc:

use jemallocator::Jemalloc;
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;

除了更改大局分配器以外,一些容器还支撑传入自界说的 allocator,比方 Vec 的完整界说是:

pub struct Vec<T, A = Global>
where
    A: Allocator,
{ /* private fields */ }

一起供给了 new_in / with_capacity_in 函数能够传入 allocator, 比方:

#![feature(allocator_api)]
use jemallocator::Jemalloc;
let mut vec: Vec<i32, _> = Vec::new_in(Jemalloc);

这样能够指示 vec 在分配内存时从指定的 allocator 里分配。不过这儿除了写起来有点费事外,更大的问题在于不是一切的数据结构都支撑自界说 allocator, 比方 String 现在就不支撑自界说 allocator。当然能够经过一些 unsafe 的代码来达成自界说分配器的方针,但无疑代码写起来要杂乱得多:

 let size = 1024;
 let layout = std::alloc::Layout::from_size_align(size, 8).unwrap();
 let ptr = unsafe { std::alloc::alloc(layout) };
 let mut s = unsafe {
     let slice = std::slice::from_raw_parts_mut(ptr, layout.size());
     String::from_utf8_unchecked(slice.to_vec())
 };
 s.push('1');
 ...

并且需求自己办理内存,不符合咱们运用 Rust 的大原则。相对来说,现在在 Rust 言语层面供给的内存分配机制不是很完善。这些问题从 2015 年就开端评论,但现在一向没有太大的开展。最近有 rust wg-allocators 工作组的人员表明要重新考虑 allocator trait(详见附录 5、附录 6),期望后续在内存分配方面有所开展。

假如对内存分配有很高的要求,咱们建议一方面能够挑选一个熟悉的分配器并依据事务场景进行调优,另一方面则是在运用层面进行优化,比方引进 arena 分配器等。

05 总结与展望

整体而言,Rust 是一门重视功能和安全的体系编程言语,经过其共同的一切权体系、借用体系和类型体系,成功地处理了传统体系编程中的许多难题。其开发者友好的语法、丰厚的标准库和强壮的社区支撑,使得 Rust 成为当今编程范畴中备受重视的言语之一。

Fabarta 这一年多的实践下来,有一些感悟希望能帮到你决议是否投入 Rust 言语:

  • 第一仍是要坚持没有银弹。 假如想经过换一门言语来处理一切问题,那么 Rust 并不能。

  • 可是,东西特别是言语会极大的影响团队才能。 简略来说,团队运用东西的才能鸿沟基本上便是团队的才能鸿沟。在这方面,Rust 供给了十分广大的范围。

  • 团队的布景决议了 Rust 的学习曲线。 出于体系级编程言语的特性,Rust 尽管不需求手动办理内存,但假如清楚背后的细节,学习曲线虽高于其他言语但并不算峻峭。

  • Rust 具有现代言语的一切特征,周边东西比方代码风格查看、测试、三方生态都十分丰厚与活泼,咱们开始对这方面有一些忧虑,但实践下来完全没有问题。

  • Rust 能够极大地提升开发功率与体系稳定性,进而给团队往前走的决心。 ArcNerual 在曩昔一年成功发布了多个大版别,全体开展甚至超过了团队之前的预期,Rust 的采用是咱们成功的必要条件之一。

Rust 仍然在高速开展,现在跟着微软、AWS 等大型商业公司的采用,拉平学习曲线、更好的异步编程、更优的内存分配、更丰厚的生态支撑等都在计划或实施中,在嵌入式、Linux 内核中也有很大的开展。关于 Fabarta 来说,咱们也将继续在 Rust 里深化并不断拓宽产品与团队才能鸿沟。

附录

1.杨成虎:存储&核算是曩昔,回忆&推理才是未来

2.一文读懂 Fabarta ArcGraph 图数据库丨技能专栏

3.探究 Tokio Runtime丨技能专栏

4.Rust Koans:users.rust-lang.org/t/rust-koan…

5.allocator trait:github.com/rust-lang/r…

6.allocator trait:shift.click/blog/alloca…

本文作者

谭宇

Fabarta 资深技能专家

现在首要专注于 AI 年代的多模数据库引擎 ArcNeural 的建设。参加Fabarta 之前曾任阿里云资深技能专家,主攻数据库、云核算与数字化转型方向。是 Tair / OceanBase 前期开发团队成员;曾担任建设阿里巴巴集团数据库 PaaS 渠道,带领团队完成了阿里巴巴数据库的容器化、存储核算别离、在离线混部等重大变革;在阿里云全球技能服务部期间提出并建设了飞天技能服务渠道,对企业数字化转型有深入的了解并有丰厚的实践经历。