学习-程序君的Rust培训1

590 阅读8分钟

特别说明

本系列都是陈天大佬在bilibili上的视频总结和学习

Slide资料 tyrchen.github.io/rust-traini…

一些有用的插件

  1. 安装Rust www.rust-lang.org/zh-CN/tools…
  2. Rust Search Extension rust.extension.sh/
  3. 画图软件 excalidraw.com/

学习就是下面这个循环过程

学习、练习、夯实知识点再学习 image.png

各种语言的优缺点

image.png

如何实现内存安全

  • 人工管理 - C/C++,很痛苦,容易出错
  • 智能指针 - C++/ObjC/Swift:性能损失,循环引用问题
  • GC - Java/DotNet/Erlang:大量的内存消耗,不必要的堆内存分配,以及(潜在的)STW
  • Ownership - Rust:范式转换(paradigm shift),学习曲线 Rust独有,通过所有权和生命周期,在编译期就消除了内存分配和回收问题

如何实现并发安全?

  • 单线程 - javascript

    • 最安全的并发是单线程并发
    • 无法有效利用多核
  • GIL - Python/Ruby

    • 一把大锁牺牲性能,换来安全
    • 锁粒度太大
  • Actor model - Erlang/Akka

    • 通过消息同步(actor -> actor)
    • 额外内存拷贝和堆内存分配
  • CSP - Golang

    • 通过消息同步(coroutine -> channel -> coroutine)
    • 额外内存拷贝和堆内存分配
  • Ownership + Type System - Rust

    • 用类型安全来保证并发安全
    • 优雅,无性能损失,且和其它方案无缝兼容

Rust如何实现内存及并发安全

以值类型为例,解释所有权。

Rust中的引用和多线程下多读单写很像

image.png

  1. 在一个作用域下,值有且仅有一个所有者,所有者离开作用域时,值被销毁
  2. 值可以有多个不可变借用
  3. 同时仅能拥有一个可变借用,可变借用与不可变借用互斥
  4. 引用的生命周期不会超出值的生命周期
  5. 通过类型系统来保证类型安全,Rust定义了很多对值和引用进行处理的trait

Send trait

一个marker trait,代表可以从一个线程安全的移动到另一个线程

Sync trait

一个marker trait,代表可以在多个线程间安全的共享

更多代码来理解所有权

image.png

image.png

小总结

第一性原理 image.png

  • 在一个 scope 中

    • 一个值只有一个所有者
    • 但可以有多个不可变引用
    • 以及唯一的可变引用(mutual exclusive)
    • 引用的生命周期不能超过值的生命周期
  • 在多线程环境下

    • 类型安全(Send / Sync)保证并发安全

使用这些简单的规则,Rust 实现了 零成本抽象 的安全

类型系统

通用类型、和类型、积类型、接口类型Trait image.png

错误处理

image.png

  1. Rust中错误分为两种,可恢复的错误以及不可恢复的错误
  2. Rust中可恢复的从错误产生、错误传递、错误处理分别有上图的处理方式
    • 错误产生:有throw exception以及return Result两种方式
    • 错误传递:有rethrow以及?操作符两种
    • 错误处理:try catch用于处理exception,match用于处理返回Result的情况

Rust可以做什么

image.png

Rust和其他语言互操作

Rust提供了多种不同的库用于和其他语言进行互操作 image.png

Rust safe与unsafe

image.png

所有权,借用检查,以及生命周期

所有权和借用检查

image.png

栈内存的生命周期

image.png

堆内存的生命周期

GC机制

image.png

ARC机制

image.png

Rust独有机制

Move 语义:保证有且只有一个 owner

image.png

  1. user这个结构体放在栈上
  2. user中的name: String是一个结构体,包含指向实际内容的指针,容量以及长度
  3. name的指针指向的内容位于堆内存中。这块堆内存是在调用to_string时,从Data区域按位copy过来的。因为字面量字符串在编译时存储在了Data区域
  4. 在调用insert函数时,user move到了insert函数中,编译器会copy一份栈内存到新的函数栈中,并标记之前栈中的user不可用,相应的他的引用也失效了
Borrow 语义:共享下的线程安全

image.png 前面3步和上面的解释相同,以下的解释界于单线程下

  1. 在调用insert函数时,使用了user的不可变借用,对应的是在insert这个函数栈2里面有一个指针指向user变量所在的栈1。由于栈2的生命周期比栈1要小,这样就保证了引用的生命周期不会大于值所有者的生命周期
  2. 函数栈2是在函数栈1之后开辟的,由于是栈结构性质,函数栈2必然先于函数栈1回退(回收),从而保证了引用的生命周期不会大于值所有者的生命周期
Rust 生命周期检查器如何避免有问题的引用?

image.png 多线程下由于ptr从Thread1移入到了Thread2,这两个Thread无法保证谁先释放,会造成T2的ptr引用一个已经释放的栈内存,从而造成无效内存访问问题

使用带有生命周期限制的借用检查器

  • 可以借用任何值(栈内存,堆内存)
  • 编译期检查(不需要耗费运行期的 CPU)
  • Rust 借用检查器基本上是个生命周期检查器

生命周期标注

image.png

pub fn strtok<'a>(s: &'a mut &str, pat: char) -> &'a str {
    match s.find(pat) {
        Some(i) => {
            let prefix = &s[..i]; // hello
            let suffix = &s[i + pat.len_utf8()..];
            *s = suffix;
            prefix
        }
        None => {
            let prefix = *s;
            *s = "";
            prefix
        }
    }
}

image.png

特殊生命周期 static lifetime

  • 'static

  • 在 bss / data / text section 中的数据

    • constants / static variables
    • string literals
    • functions
  • 如果在类型的 trait bound 中使用:

    • 类型不包含任何非静态的引用(non-static references)
  • 有所有权的数据的 lifetime bound 是 'static,引用数据不是

RAII 概念

  • 对象的初始化会保证资源的初始化
  • 对象的释放会保证资源的释放
Rust中通过Drop trait实现了RAII

主要包括以下几类

  • memory
  • file
  • socket
  • lock
  • any other OS resources

总结:Cost of defects

  • 代码中不引入任何缺陷(只要是人,就无法避免错误)
  • 在敲入代码的时候就能被清晰地告知代码中的缺陷(发现问题时间:毫秒级)
  • 在编译或者单元测试时发现(秒级/分钟级)
  • 代码被 push 到 PR 中,CI 发现错误(分钟级/天级)
  • 别人 review 你的代码时发现问题(小时级/天级)
  • 代码 merge 到 master,更严格的 CI(比如 end-to-end test)发现错误(小时级/天级)
  • 代码被部署后回归测试发现问题(天/周/月)
  • 代码被部署后很久用户发现问题(周/月/年)

类型系统和泛型编程

image.png

image.png

Rust中的trait

image.png

  1. trait和其他语言中的interface或protocol相似
  2. trait可以有默认实现
  3. 为一个类型实现某个trait需要遵循孤儿原则:trait或者类型是当前create中新增的

trait object

image.png

  1. Rust会为每个Trait+Type实现一个trait object元类
  2. 使用trait object的时候,他原本类型会被抹掉,我们只知道它符合某个trait

image.png

More about trait

image.png

  1. trait可以有关联类型以及泛型类型
  2. trait可以继承
  3. trait可以组合,通过+号。Swift中protocol通过&进行组合

泛型

image.png

image.png

image.png

泛型编程例子

C++中的泛型例子 image.png Rust中的泛型例子 image.png

并发

image.png

image.png

image.png

image.png

image.png

多线程

Mutext

image.png 问题:

  • 原子性

    • 在多核环境下,1/2 之间会产生竞争(race condition)- 其它线程也许会进入
    • 在单核环境下,OS 抢占多任务依旧可能会在 1/2 见产生竞争
  • 乱序

    • 编译器也许会优化指令,把 3 放在 1 之前
    • CPU 也许会乱序执行(OOO execution),把 3 放在 1 之前

如何解决这个问题?

  • 需要硬件(CPU)来保证原子性和避免 OOO
  • 算法:CAS (Compare-And-Swap)
  • 数据结构(Rust):AtomicXXX

image.png

Ordering 是什么概念?

  • Relaxed: 没有限制,随意 OOO

  • Release:

    • 对当前线程,任何 read/write 不能被乱序到这条指令之后(比如:store
    • 对于其它线程,如果用 Acquire read,会看到变化后的结果
  • Acquire:

    • 对当前线程,任何 read/write 不能被乱序到这条指令之后(比如:compare_exchange
    • 对于其它线程,如果用 Release 来更新数据,更新的数据会被当前线程看到
  • AcqRel: Acquire 和 Release 的结合

  • SeqCst: AcqRel 之外,所有线程都看到相同的操作顺序

image.png

image.png

实际的Mutex工作模式

image.png

Semophare,更一般化的mutext

image.png

Channel

Channel基础

image.png

  1. 根据生产者和消费者的数量,分为四种SPSC,MPSC,SPMC,MPMC
  2. 通过一个RingBuffer实现
各种channel的实现
  • sync: 容量有限,发送者会被 block

    • Mutex + Condvar + VecDeque
    • Atomic VecDeque (atomic queue) + thread::park + thread::notify
  • async: 容量无限,发送者不会被 block

    • Mutex + Condvar + VecDeque
    • Mutex + Condvar + DoubleLinkedList
  • rendezvous: 容量为 0,用于线程间同步

    • Mutex + Condvar
  • oneshot: 只允许发送一次数据 e.g. Ctrl+C to stop all threads

    • atomic swap
  • async/await

    • 和 sync channel 类似,但 Waker 不同

异步操作

async、await实现

image.png

  1. 非常优雅,async编译后,返回值就是一个impl Future
  2. Future trait有一个关联类型Output

Pin

image.png

声明宏

  • macro_rules!
  • #[macro_use] / #[macro_export]

过程宏

  • 类函数宏(function-like macros):println!(...)
  • 派生宏(derive macros):#[derive(Debug)]
  • 标记宏(attribute macros):#[tokio::main]

资料链接

【程序君的 Rust 培训(1)】www.bilibili.com/video/BV19b…

【课件】tyrchen.github.io/rust-traini…

【代码】github.com/tyrchen/rus…

【Sum Type 和 Product Type】www.modb.pro/db/111525