Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

简单聊聊编程语言的哲学,以及关于 Rust 的一些想法 (1) #994

Closed
qiwihui opened this issue Feb 13, 2021 · 1 comment
Closed
Labels
Milestone

Comments

@qiwihui
Copy link
Owner

qiwihui commented Feb 13, 2021

感觉这样下去真的不行,如果每次动笔都是起码五千字——意味着好几天的全天候无休码字、占用了所有的空闲时间、导致长久的眼睛酸疼的话,很快就要丧失更新博文的动力了…… 😥😥😥



Tags:



via Pocket https://ift.tt/3aZ57uN original site



February 13, 2021 at 07:36AM

@qiwihui qiwihui added this to the 2019阅读 milestone Feb 13, 2021
@github-actions
Copy link

简单聊聊编程语言的哲学,以及关于 Rust 的一些想法 (1) by @nagisakaworu17

2021-02-12发表programming约8482字

简单聊聊编程语言的哲学,以及关于 Rust 的一些想法 (1)

本文是一篇「小作品」。

草,写着写着发现越写越长,一点也不「小」嘛。

感觉这样下去真的不行,如果每次动笔都是起码五千字——意味着好几天的全天候无休码字、占用了所有的空闲时间、导致长久的眼睛酸疼的话,很快就要丧失更新博文的动力了…… 😥😥😥

我应该真的尝试一下「小」作品的体例才是。

我的长期TODO列表里已经躺着五六篇以“博文”开头的条目——原本想着寒假一周一篇很快就能写完,然而到现在也没动笔。爆肝填坑了一个星期,今天实在有点累,不大想打开 RustLion,于是把这篇坑了很久的文章写一写。

在这几篇坑了这么久的文章中其实有一篇已经写了前半部分了,然而咕了太久后半部分要写什么都有点不大记得,于是只能前功尽弃…… 😥😥

本文的主要内容是从我个人的经验出发,简单聊聊对于 Rust 的一些想法和体会。我会尽量避开诸如 “文档质量良好”、“很有特点” 这类宽泛的概括,而尽量将自己在使用 Rust 编程的过程中感受到的一些特别之处、尤其是和此前经历的不同之处拿来说说。我期望如此行文能使得本文对无论是 Rust 初学者、还是仍在观望的开发者甚至是 Rust 老手们都能带来一定启发。

本文将主要按这些体会主要关注的语言侧面组织,每一点之间的内容基本独立,我将尽量涉及从语言哲学到线程安全的实现细节等多个方面和层次的内容。

由于篇幅原因,本文将分截为两到三篇文章发出。其实原本不想写这么多……. 😥😥😥 以下是文章内容的粗略概要(很可能有变动):

  1. 编程语言的哲学
  2. Rust: “be explicit”
  3. 复杂性
    1. “子语言”
    2. 并发和线程安全
  4. 第一部分:主要引述来源
  5. 继承 V.S. 组合:另一种思维方式
  6. Typeclass 和 mod
  7. 元编程和宏系统
  8. 结语:“language adopted to fill a niche”
  9. 第二部分:主要引述来源

本文将包含以上概要的第一到三部分。其实原本真的真的不想写这么多……. 😥😥😥😥😥😥

  Cover image by  かざり  on  Twitter.

编程语言的哲学

一直以来我都认为,一门计算机语言的哲学对语言多个方面的特性都有着非常重要的影响,理解语言的设计哲学对理解语言为何如此有着重要的意义。比如,个人认为:

  • Java / JVM:打造大规模企业级程序的首选平台

    这一定程度上解释了 Java 选择较重的运行时(因而免除了手动内存管理的麻烦),并且为了在具有较重运行时的前提下同时达到企业级程序要求的 “处理大量数据时的性能开销仍然可以忍受”,Oracle 在 JVM JIT 以及GC的设计和实现上投入很大。Zing VM 拥有目前市面上性能最强的GC器之一(据此处,Zing VM 的 C4 可以在管理高达2TB堆内存、外加10TB堆外内存时仍然保持 5ms 以内的并发GC停顿),而新近出现的几个Hotspot GC 器(Shenandoah、ZGC)的工作表现同样相当不错(Grammerly 报告工作在生产环境的 Shenandoah 在 10k req/s 的 QPS 和 60GB 的堆内存下仍然具有 <10ms 的GC停顿(hicuup),见此处)。另一方面,据 Mark Reinhold 所述,Hotspot JVM “目前70%-80%的工作都是在C2(第二级JIT)上”,大量研究表明经过深度JIT优化的字节码可以达到和原生代码同等、甚至更高的效率(GraalJS 是使用 Java 实现的 JavaScript 运行时,它工作在为高效执行跨多个语言的应用而特别设计的虚拟机 GraalVM 上,近期有报告指出深度优化后的 GraalJS 可达到优于 V8 的执行效率(参见此处),这个结果还是蛮搞笑的……)。

    重要的一点观察是,通过较重的运行时,开发人员可以将注意力更好地集中在业务本身,而非各种内存管理策略带来的、通常是令人气馁的复杂性和安全问题。此引文,尽管业界已在大量静态分析和动态检查工具(如 CppcheckValgrind Memcheck 等)上花费了数亿美元,并付出了同样艰苦的努力制定编程规范并培训程序员们,每年仍然有超过70%的软件漏洞(CVE)与内存安全问题有关。从这个角度上来说,托管语言(managed language)天生地比非托管语言(unmanaged language)更好地适合大多数的应用场景——在这些场景下,开发人员生产代码仅仅是为了解决常规的业务问题,有关代码优化、大内存管理等等细节,交由较重的运行时、进而转嫁给专门处理这些复杂方面的虚拟机专家们就好了吧。

    在这个问题上,Rust 选择了一条独特的道路:它通过严格的编译规则和精心设计的标准库达成了通过编译器强制保证的内存安全性——如此既没有内存不安全的风险,又没有运行时的性能开销。然而,这样的设计加重了程序员的心智负担,并带来了主要的复杂度——对于编译器和编程人员皆是如此——和“比较陌生的方面”,而这样的复杂度和我们通常所说的 “复杂” (Scala 式的 “复杂”)之间存在一定区别,这点会在下文中展开来讲。

  • Scala:用最少的特性,做最强的语言

    Scala 是一门编译到 JVM 字节码的多范式语言。在所有工业级编程语言中,Scala 以其惊人的复杂度和优雅程度而著称。个人认为,Scala 在考虑语言设计时的重要方面是更加重视特性之间的 “联动”:如果能通过几个更加基本的特性来实现一个需要的特性,Scala 会优先选择更好地支持这几个小特性,而不是为了某一个常见的编程 idiom “开近道” ——这样的语言设计有时能够获得非常好的“联动”效果,因为几个小特性各种各样的组合方式通常比几条近道能表达更多的东西,当然也要复杂得多。一个非常好的例子是 Scala 中的枚举:大多数语言中的枚举(如 Java、Kotlin、C# 等)都是作为一个特殊的语法(enum class)来实现,它由编译器负责展开;然而 Scala 通过两个更加基本的特性(类型别名和继承)实现了枚举这一特性,并未在语法层面提供任何特殊支持。相似的例子还有类型标识符由类型投影这一个小特性实现,而类型投影又和结构化类型发生“联动”最终产生了类型 lambda 这一堪称神奇的特性。

    简单展示一下这个神奇的存在,通过 typeclass 范式为 Map 类型实现 map 函数:

    1
    2
    3
    4
    5
    6
    implicit class MapFunctor[K, V1](mapV1: Map[K, V1])
    extends Functor[V1, ({type λ[α] = Map[K, α]})#λ] {
    def map[V2](f: V1 => V2): Map[K, V2] = mapV1 map {
    case (k, v) => (k, f(v))
    }
    }

    看到那串 λα 了吗?这,就是 Scala。

    不过这串代码依然给我一种非常优雅的感觉。目前能让我感觉十分优雅的还是只有Scala(和 Ruby?)……

    一直没有机会拿来写点复杂项目啥的,其实感觉都要忘光了…… 哭哭

    算了,等 Scala 3 用起来了再完全重学吧((((

  • Ruby:极致的脚本语言

    Ruby 的许多特性强烈地偏向对脚本语言的良好支持。在我看来几乎从任何一个可以想象的方面来说 Ruby 都要比 Python 更加合适作为脚本语言的默认选择…… 比如 Ruby 支持任意地方法覆写,你甚至可以覆写在 Int 中定义的 + 方法从而立刻把REPL给崩掉… 比如 Ruby 非常好地普及了守护(guard)的使用,从而使得大量的 idiom 有了 one-liner;比如这是一门动态类型语言,编译器只会阻止很少的事情;比如 Ruby 简洁的闭包语法(事实上 Rust 中的闭包语法正是沿袭自 Ruby)、do 语句以及对各种常用集合操作的良好支持,使得以闭包作为参数的标准库API基本可以替代循环;比如各种高度动态的语言特性和内省(introspect)机制。良好和高度动态的设计使得 Ruby 不仅适合编写各种 dirty but works 脚本,同样也适合工业的快速成型。

    希望 Python 早日凉透。(光速逃

Rust: “be explicit”

Rust 一点重要的设计考虑,在于 be explicit。或许是看到了 C++ 长期坚持在各种问题上走隐式优先的策略如今吃了太多苦头(比如隐式类型转换一向以来被视作是 “bug 的一大来源”,甚至还有 std::string str = 12; 这种毒瘤写法…… 而且似乎删除函数也没帮上什么忙(😒 )于是 Rust 选择了要求程序员明确指明绝大部分转换的写法——即 “be explicit”。作为一门侧重在受限资源环境下尽量保持极低性能消耗的语言,Rust 要求对资源的创建必须显式声明。这一点首先以 Rust 中全局采用移动语义(move semantics)和严格的引用/借用检查器为基础,再通过标准库中的的一些 trait(Typeclass 式的)强制资源创建的显式声明。最为代表性的例子自然是 to_owned

1
2
let str: &str = "Hello";
let str: String = str.to_owned();

str 类型作为编码在二进制文件中的字符串,其类型显然应为这一串二进制字符串的一个 slice(即 &str)。它不在堆上占据空间,而仅仅是一个指向文件中某一段的指针;自然而然,对一个指针执行字符串拼接是不可能的——于是在 Rust 中,连续地拼接字符串不仅语法繁琐,而且还需要对语言有一定的熟悉:

1
2
3
4
5
6
let s1 = "hello";
let s2 = " ".to_owned();
let s3 = "world";

let concat = s1.to_owned() + &s2 + s3;
println!("{} {} {} -> {}", s1, &s2, s3, concat);

Rust 要求在各种引用、分配内存时显式声明,这使得程序员更加关心引用的传递以及资源的消耗:以程序员的更高心智负担为代价,Rust 不断提醒着程序员注意资源分配,通过语法的冗长督促提醒程序员尽量减低资源使用。

Rust 同样具有一些程度的隐式(implicit ,注意不是 Scala 语境下的那个 implicit):如通过 Copy trait 实现隐式的复制语义、解引用强制多态(deref coercions)等。Rust 谨慎控制了这些隐式的适用范围,前者仅适用于少数复制成本极低的内建类型(而如 String 一类有较高潜在复制成本的类型则需要通过 Clone trait 显式指定和调用;CloneCopy 的分立是 Rust 在 be explicit 哲学语言易用性 之间取得的良好折衷),后者则仅适用于函数和方法传参。

有时候,复杂的分配声明、引用和解引用转换会导致冗长难读的代码。Rust 通过一些标准库 trait 部分缓解了这一负担,并通过宏(macro)系统(下文会更深入地涉及)为一些常见范式提供了全兼容的写法,将这一负担转嫁到宏的实现中。比如,众所周知,大家都不喜欢用 + 做字符串拼接,*in Rust, we use format!*:

1
2
3
4
5
6
let s1 = "hello";
let s2 = " ".to_owned();
let s3 = "world";

let concat = format!("{}{}{}", s1, s2, s3);
println!("{} {} {} -> {}", s1, &s2, s3, concat);

这一点哲学,是 Rust 的首创吗?我想不是。写到这里,我在想,是不是所有与ML系(或者说 Haskell 系?)语言的元素(如 Typeclass、ADT through datatype + pattern matching with exhaustiveness checking 等)有强烈关联的语言都更加倾向于 be explicit。Rust 如此;Haskell 如此;C++ 20 的一个失败的 concept 的设计草案(被称为 Indiana Concept)同样如此(我觉得这个草案被否决某种上是因为在一门本来鼓励隐式的语言中去推行 be explicit 只会导致如 late_check 这样明显权宜之计的简陋方案,即是说 Typeclass 式的哲学与 C++ 本身的哲学并不契合是导致 Indiana Concept 未能取得成功的重要原因);SML 做得还要更绝,它甚至不允许对实数进行判等,因为这样的判等本身隐含了一种对精度的约省。

这样一簇语言设计思路为什么都走向了 be explicit 这一哲学?这一哲学是必然隐含在人们通常界定的 “ML味”(即:带类型推导和参数化多态(但没有子类型多态)的静态类型系统,即 Hindley-Milner 类型系统;datatype 支持的 ADT 以及模式匹配(pattern matching)和解构句法;一个模块子语言,它提供了巧妙的隐藏机制且某种程度上正是一种 Typeclass 范式的实现(关于这点可能会另写一篇文章简单说说),即人们常说的 ML module system)中的吗?还是说,这仅仅是同一帮人带着同样的思想和哲学反复参与到各种语言及语言草案的设计中?

比如说,Dan Grossman 就说:comparing real numbers with == is a really bad idea.

然而我其实觉得还好……

其实如果正确规定浮点数的判等(比如说,$a-b = \delta < \epsilon ;=10^{-5}$)并在文档中广而告之这一默认行为,或者强制要求程序员在执行浮点数判等时必须指定 $\epsilon$(指定采用 “默认值” 也算指定),可能会是一个解决方案…

然而有没有人买账呢。:-(

其实老实说,在我个人的编码工作中是极少遇见浮点数的。或许根据众所周知的编程语言设计原则让那些罕见的操作(比如浮点数判等)繁琐但精确会不会是一个不错的选择?🤔

复杂性

我想大家应该都会同意,Rust 是一门复杂的语言。但是 Ruaaast 的复杂性并不在于这一门语言表达力多么惊人、程序员能够接触到多少新的特性、进而将此前多少无法简洁表达的抽象统统实现,而是在于 Rust (出于各种权衡)引入的独特所有权和线程安全机制将大大挑战程序员以往的思维——或者说,Rust 带来的主要的复杂度和陌生之处,并不用于向程序员的工具箱中增添多少强力的工具(事实上,个人认为,很大程度上 Rust 的表达力并未显著超出ML系语言的一般水平,详见下文),而是用于解决很多其它语言中并不存在的问题。

Rust 具有复杂的类型系统。在 Rust 中,不仅值和引用(以及和引用的引用,引用的引用的引用,…)不是同一个类型,不符合生命周期约束的引用,同样不是同一个类型,不匹配的传参将会被编译器拒绝(生命周期 “匹配” 的规则,能不能看作是生命周期的 subtyping…?比如,&'static 总能符合各种生命周期约束)。

“子语言”

借用知名PLT大V千里冰封在某次技术分享中的话语,那就是和绝大部分计算机语言一样,Rust 同样也是由很多门子语言组成的。简单列举一下,我们可以得到:

  • 表达式子语言规定了表达式(expr)的句法、类型检查规则和执行规则(这三点其实是 Dan Grossman 在课程 Programming Languages 中提出的:syntax, type-checking rules, evaluation)。
    重要的一点观察是,很多语言都有表达式子语言,或者说没有语言没有表达式子语言。对于子语言的拆解有重要的一些考虑,详见下文。
  • 模块(mod)子语言指明相对独立的命名空间相互暴露和引用的方式。众所周知,这在 Rust 中是通过 mod 的引用(use)来实现的。
  • 多态子语言(注意,此处的「多态」指 polymorphism,即 “参数化多态”、“子类型多态” 意义上的多态,而非 Java 语境下的那一种多态(对于 Java 语境下常被提及的多态,我想更多地应该是指动态分发 dynamic dispatch)…)为语言提供了一种表达力,它使得同一段代码能够应用于各种不同的类型。向这些类型施加限制(并转化为相对于这段代码而言的 “已知条件”)的方式、语法和规则,由这门子语言指定。C++ 中采用的模板,即所谓的 特设多态 ad-hoc polymorphism(Bjarne 本人似乎喜欢把这个叫做 “泛型编程 generic programming”,但为了避免和 Java 式的那种泛型混淆我们还是称多态好了(然而 “多态” 也容易和那种 Java 式的那种多态(实际上是 动态分发 dynamic dispatch)搞混嘛((((😒 所以最终结论是 Java 的广泛流行对各种术语的界定真是后患无穷(((( (光速逃🤯 )、Rust 和 Java 存有一定近似(别打我)的泛型系统(尤其是在约束这一层次上)、ML系语言中的参数化多态('a),皆属此项。

    Rust 的多态子语言基本通过基于 trait 的可加和 context bound 施加限制(e.g. T: Clone + Debug),这在 Java 中可以通过基于 interface 的 bounded type paremeter 近似达成(e.g. T extends A & B)。个人认为在这一层次上 Java、Scala、Kotlin、C# 一类的语言由于具有子类型化(subtyping)要更加复杂,它们需要处理诸如逆变和协变(还有存在类型 existential type)一类复杂的问题(并导致大量微妙的设计差异,比如 Java 的定义处类型变异和 Kotlin 的声明处类型变异,显然后者是更优解)甚至有时候还有数组协变这样的难搞历史包袱。

    其实这段有点扯淡…… 因为 Rust、Haskell、Scala 式的 Typeclass 原语允许一个新增的类型实现(分别通过 impl 块、隐式转换 implicit conversion、instance 块)某一个预先定义的 typeclass(虽然这些语言中一般都会 孤儿规则 orphan rule 限制这样的 “实现” 的适用范围),而以 Java 为代表的一类语言中的 interface 其实是不具有这种能力的。

    这其实是对著名的 表达式问题 Expression Problem 的解的探讨…… 某种程度上个人认为 typeclass 其实是对这个问题一个很好的解,关于这个问题以及不同语言(Haskell、Scala、Rust;C#、Kotlin;Ruby)的不同解法(Typeclass;扩展函数;mixin)有空在以后的博文中再聊. 😋

    另一方面,Rust 有一些它需要关心的特殊问题。由于 Rust 中对象创建时的内存分配由编译器完成,Rust 引入了 marker trait Sized,它指明被标记的类型的大小在编译期可确定(这是程序能够通过编译的重要条件),并默认所有的类型(除非特殊声明)均是 Sized 的。

    Rust 实现这一点的方式是,向语言中引入了新的语法 unsafe auto trait。当一个 marker trait 被声明为 auto 时,编译器会自动为所有类型实现该 marker trait ——除了预先指定的那些反例,如 impl !Trait for Type。综合来看,这一套语法要求编译器为反例之外所有类型自动生成对某一 marker trait 的实现。

    为了使一些 context bound 能够适用于 Sized 的类型,Rust 又引入了语法 ?Sized。当它出现在一个 context bound 中(如 T: ?Sized)时,表明限制该类型(T)为 Sized 或非 Sized 的。(某种程度上 ?Sized (而非 T)才表示 Rust 中的 “所有类型” ?)

    下文将要提到的 SendSync 也是通过这样的方法实现的。

    个人认为,问题在于(花了超多时间和笔墨讲前面的这几点可却仅仅是个铺垫…… 超级累啊 😣😣 ),这一部分语言设计并不算好。表观的定性认识是,Rust 专为一些基本可以说是 实现细节 的东西专门提出了几条语法,并且将这几条语法的适用范围牢牢限制在它们之所以被发明出来的那一部分——即是说,这一部分语言设计,几乎不可能与语言的其它部分产生联动,而良好的特性联动无疑是语言设计上佳的重要标志。另一方面,出于类型系统完备性(soundness)等各种各样的考虑,这些 auto trait 在语言各个层面上的工作方式(比如,supertrait,模式匹配等)均和一般 trait 存在差异——这与良好语言设计的另一个标志,即 “创造尽量少的特例” 相悖。个人的认识是,这些 auto trait 是贯穿 Rust 语言的几个深“洞”,它们要求语言各处为它们制定独一无二的规则,并且将这些规则牢牢限制在它们本身。

    除此以外,由于 ?Sized 肩负着 “所有类型” (存疑?) 的职责,这一极其局限的语法在大量代码中均有出现(比如标准库、tokio、一些第三方集合库,等等)。将一个常见且重要的职责赋予一条怪异、适用性低、并不直观的语法,长久以来提高了初学者的理解负担(“依赖里头突然看到了个这个,它到底是什么意思啊?”),并使人们不断提出这样的问题:为什么这三条语法不能广而推之?于是我们看到有关 negative bounds 的语言设计提案(RFC)和讨论被不断提出,语言设计者们不厌其烦地解释为什么这是个坏主意,以及为何更加细致考虑的特性(如 specilization)才是正途。

    其实无论如何,Sized 都是一个只有 Rust 才会面临的特殊的难题——正如上文所述,这部分复杂度是为了解决一个其它语言中并不存在的问题。这部分设计,将原本就是泛型的意义强行嵌入到某一个 trait 中(“T 不就是所有类型吗?为什么还要造一个 T: ?Sized 呢?”),无论如何,这个问题难以优雅地解决。

  • 生命周期子语言是 Rust 中极为特殊的一部分:同样,它也是为了解决其它语言中并不存在的问题。Rust 要求在 结构体 struct——注意,Rust 的结构体 完全不是 C / C++ 中的结构体——中出现的引用必须具有生命周期标记,语法借用了泛型中类型参数的语法:

    1
    2
    3
    4
    5
    struct BorrowedPoint<'a> {
    name: &'a str,
    x: &'a i32,
    y: &'a i32,
    }
    1
    2
    3
    fn print_multi<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("`print_multi`: x is {}, y is {}", x, y);
    }

    尽管 Rust 已经实现了各种各样的生命周期推导规则和尽量复杂的生命周期分析(称作 non-lexical lifetimes),很多时候仍然需要程序员手动标明生命周期标记。对于结构体的生命周期标记尤其让人头疼:带引用的结构体 的生命周期标记是强制要求的,不能省略;而一个结构体上的标记会扩散到整个代码库中,即便对于能够自动推断得出的生命周期,也要使用生命周期标记省略语法 BorrowedPoint<'_> 注明。这使得相关的重构要更加复杂。

    我仍旧相当惧怕在结构体中使用引用。很多时候,我并不能确切得出一个结构体到底是否适合包含引用的结论(这方面是否有一些指导原则和官方文档?),而贸然执行重构得来的是大规模的编译错误、繁琐的修正,有时最终又发现并不适合使用引用——典型的情况是在跨越线程边界时。

    在很多情况下(个人经验是以 带引用的结构体 为主)仍然需要手工标注生命周期,而这一点仍然、并持续带来显著的心智负担。

    自然要问的问题是,仅靠对程序的 静态分析 static analysis,我们能够将程序中每一个引用的生命周期分析到什么地步?即是说,Rust 的生命周期推导本身、以及类型系统和生命周期系统的配合,是否还有更加完善以更进一步降低程序员心智负担的可能?

    我仍然相当盼望不再强制带引用的结构体必须带生命周期标记的那一天。

    除此以外,生命周期的继承机制(如:'a : 'b)和一些特殊生命周期(&'staticT: &'static ——注意,尽管两者语法极其相似,但语义却有重要差异,这是初学者容易犯的一个常见错误)的存在使这一部分的语言更加复杂和难以理解。

    以上所有,在 Rust 的多种生命周期传递方式(函数/方法参数、结构体、闭包捕获等)各异的局限下,创造出多处特例,使得它的工作方式更加难以归纳和掌握。

可以看得出来,某种程度上本段对冰冰的 “子语言” 观点(不要打我)仅仅是作名词意义上的借用——和基于 PLT 研究风格的分析不同,本段的这种拆分与其说是在语言研究上有着什么启发性意义,不如说仅仅是将一门语言分块来说会更加方便而已。关于他的这部分观点的更多内容,详见文末 “主要引述来源”。

并发和线程安全

我真的好累,眼睛还疼学校里还没有人喜欢我 😥😥😥,所以这个部分只能简单写写算了。

在一门没有运行时的语言里实现易用的并发是有挑战性的——特别是线程安全的并发。Rust 通过向标准库中引入几个设计精巧的 API —— SyncSendPin 等——较好地解决了这个问题。Rust 一个重要的并发库 tokio 生态完整(包括基本协程支持、同步原语(信号量 semaphore通道 channel)、流式并发、网络和网络包解析、结构化日志等等)、文档质量优异(事实上 tokio 的教程是我所了解过的开源项目中质量最高的之一 🤩),一定程度上简化了在 Rust 中实现线程安全并发的困难。

从一般工程实践上看,在 Rust 中编写良好的线程安全并发,需要了解的包括:

  • 一些基本的同步原语:信号量 semaphore通道 channel互斥锁 Mutex读写锁 RwLock
  • 一些 Rust 特有的、为保证线程安全而特别设计的 auto marker trait: SyncSend
  • 一些 Rust 标准库 trait 和相关机制存在的必要性及用途:Pin / UnpinBox::pintokio::pin
  • 特殊生命周期 &'static 以及 T: &'static 的含义(尤其是这两者如何不同
  • 以及一些智能指针(其存在的必要性和局限,横向的比较,性能开销等等):BoxRcArcRefCell

小问题:

1
2
3
type MessageStream = StreamMap<String, Pin<Box<dyn Stream<Item=Arc<PUBLISH>> + Send>>>;
type Publishers = Box<dyn PublisherRepository + Sync + Send>;
type SyncSessionManager = RwLock<SessionManager>;

试解释以上类型别名中各个 API 出现之用途?

除此以外,如果需要深入理解 Rust 并发的实现原理,还需要了解 Futuer / WakerScheduler 等。个人体会是并发库中各种并发操作的实现,由于 Future / Waker 范式本身固有的复杂性和理解难度以及惯用实现风格的原因,相关代码并不好读;这点在研究 poll 簇并发操作等方面时会带来一些困难。

在任何编程语言中实现线程安全的并发都需要细致的设计考虑和复杂的工程实现。Rust 社区呈现出如此完全的生态和易用程度,足以为开发人员和软件生产带来良好的体验,并为其它编程语言的设计带来重要启发。

第一部分:主要引述来源

以下简单列出本部分文章主要引述资料的来源。有一部分引文已在原文中以超链接形式给出,故在此处略去。

本文就这么。感觉其实没讲什么很有意思的东西。。。。然而我真是要累死了 😑😑

希望还有机会写完本文。祝大家新年 Ruaaaaaaaaast 愉快!

<全文完>

简单聊聊编程语言的哲学,以及关于 Rust 的一些想法 (1)

https://ray-eldath.me/programming/thoughts-on-rust-1/

作者

Ray Eldath

发布于

2021-02-12

更新于

2021-02-12

许可协议

@github-actions github-actions bot changed the title 简单聊聊编程语言的哲学以及关于 Rust 的一些想法 (1) 简单聊聊编程语言的哲学,以及关于 Rust 的一些想法 (1) Feb 13, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant