特别说明
本系列都是陈天大佬在bilibili上的视频总结和学习
Slide资料 tyrchen.github.io/rust-traini…
一些有用的插件
- 安装Rust www.rust-lang.org/zh-CN/tools…
- Rust Search Extension rust.extension.sh/
- 画图软件 excalidraw.com/
学习就是下面这个循环过程
学习、练习、夯实知识点再学习
各种语言的优缺点
如何实现内存安全
- 人工管理 - 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中的引用和多线程下多读单写很像
- 在一个作用域下,值有且仅有一个所有者,所有者离开作用域时,值被销毁
- 值可以有多个不可变借用
- 同时仅能拥有一个可变借用,可变借用与不可变借用互斥
- 引用的生命周期不会超出值的生命周期
- 通过类型系统来保证类型安全,Rust定义了很多对值和引用进行处理的trait
Send trait
一个marker trait,代表可以从一个线程安全的移动到另一个线程
Sync trait
一个marker trait,代表可以在多个线程间安全的共享
更多代码来理解所有权
小总结
第一性原理
-
在一个 scope 中
- 一个值只有一个所有者
- 但可以有多个不可变引用
- 以及唯一的可变引用(mutual exclusive)
- 引用的生命周期不能超过值的生命周期
-
在多线程环境下
- 类型安全(
Send/Sync)保证并发安全
- 类型安全(
使用这些简单的规则,Rust 实现了 零成本抽象 的安全
类型系统
通用类型、和类型、积类型、接口类型Trait
错误处理
- Rust中错误分为两种,可恢复的错误以及不可恢复的错误
- Rust中可恢复的从错误产生、错误传递、错误处理分别有上图的处理方式
- 错误产生:有throw exception以及return Result两种方式
- 错误传递:有rethrow以及?操作符两种
- 错误处理:try catch用于处理exception,match用于处理返回Result的情况
Rust可以做什么
Rust和其他语言互操作
Rust提供了多种不同的库用于和其他语言进行互操作
Rust safe与unsafe
所有权,借用检查,以及生命周期
所有权和借用检查
栈内存的生命周期
堆内存的生命周期
GC机制
ARC机制
Rust独有机制
Move 语义:保证有且只有一个 owner
- user这个结构体放在栈上
- user中的name: String是一个结构体,包含指向实际内容的指针,容量以及长度
- name的指针指向的内容位于堆内存中。这块堆内存是在调用to_string时,从Data区域按位copy过来的。因为字面量字符串在编译时存储在了Data区域
- 在调用insert函数时,user move到了insert函数中,编译器会copy一份栈内存到新的函数栈中,并标记之前栈中的user不可用,相应的他的引用也失效了
Borrow 语义:共享下的线程安全
前面3步和上面的解释相同,以下的解释界于单线程下
- 在调用insert函数时,使用了user的不可变借用,对应的是在insert这个函数栈2里面有一个指针指向user变量所在的栈1。由于栈2的生命周期比栈1要小,这样就保证了引用的生命周期不会大于值所有者的生命周期
- 函数栈2是在函数栈1之后开辟的,由于是栈结构性质,函数栈2必然先于函数栈1回退(回收),从而保证了引用的生命周期不会大于值所有者的生命周期
Rust 生命周期检查器如何避免有问题的引用?
多线程下由于ptr从Thread1移入到了Thread2,这两个Thread无法保证谁先释放,会造成T2的ptr引用一个已经释放的栈内存,从而造成无效内存访问问题
使用带有生命周期限制的借用检查器
- 可以借用任何值(栈内存,堆内存)
- 编译期检查(不需要耗费运行期的 CPU)
- Rust 借用检查器基本上是个生命周期检查器
生命周期标注
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
}
}
}
特殊生命周期 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)发现错误(小时级/天级)
- 代码被部署后回归测试发现问题(天/周/月)
- 代码被部署后很久用户发现问题(周/月/年)
类型系统和泛型编程
Rust中的trait
- trait和其他语言中的interface或protocol相似
- trait可以有默认实现
- 为一个类型实现某个trait需要遵循孤儿原则:trait或者类型是当前create中新增的
trait object
- Rust会为每个Trait+Type实现一个trait object元类
- 使用trait object的时候,他原本类型会被抹掉,我们只知道它符合某个trait
More about trait
- trait可以有关联类型以及泛型类型
- trait可以继承
- trait可以组合,通过+号。Swift中protocol通过&进行组合
泛型
泛型编程例子
C++中的泛型例子
Rust中的泛型例子
并发
多线程
Mutext
问题:
-
原子性
- 在多核环境下,1/2 之间会产生竞争(race condition)- 其它线程也许会进入
- 在单核环境下,OS 抢占多任务依旧可能会在 1/2 见产生竞争
-
乱序
- 编译器也许会优化指令,把 3 放在 1 之前
- CPU 也许会乱序执行(OOO execution),把 3 放在 1 之前
如何解决这个问题?
- 需要硬件(CPU)来保证原子性和避免 OOO
- 算法:CAS (Compare-And-Swap)
- 数据结构(Rust):
AtomicXXX
Ordering 是什么概念?
-
Relaxed: 没有限制,随意 OOO
-
Release:
- 对当前线程,任何 read/write 不能被乱序到这条指令之后(比如:
store) - 对于其它线程,如果用
Acquireread,会看到变化后的结果
- 对当前线程,任何 read/write 不能被乱序到这条指令之后(比如:
-
Acquire:
- 对当前线程,任何 read/write 不能被乱序到这条指令之后(比如:
compare_exchange) - 对于其它线程,如果用
Release来更新数据,更新的数据会被当前线程看到
- 对当前线程,任何 read/write 不能被乱序到这条指令之后(比如:
-
AcqRel: Acquire 和 Release 的结合
-
SeqCst:
AcqRel之外,所有线程都看到相同的操作顺序
实际的Mutex工作模式
Semophare,更一般化的mutext
Channel
Channel基础
- 根据生产者和消费者的数量,分为四种SPSC,MPSC,SPMC,MPMC
- 通过一个RingBuffer实现
各种channel的实现
-
sync: 容量有限,发送者会被 block
Mutex+Condvar+VecDeque- Atomic VecDeque (atomic queue) +
thread::park+thread::notify
-
async: 容量无限,发送者不会被 block
Mutex+Condvar+VecDequeMutex+Condvar+DoubleLinkedList
-
rendezvous: 容量为 0,用于线程间同步
Mutex+Condvar
-
oneshot: 只允许发送一次数据 e.g. Ctrl+C to stop all threads
- atomic swap
-
async/await
- 和 sync channel 类似,但
Waker不同
- 和 sync channel 类似,但
异步操作
async、await实现
- 非常优雅,async编译后,返回值就是一个impl Future
- Future trait有一个关联类型Output
Pin
声明宏
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…
【Sum Type 和 Product Type】www.modb.pro/db/111525
-
Ordering: doc.rust-lang.org/std/sync/at…
-
std::memory_order: en.cppreference.com/w/cpp/atomi…
-
Atomics and Memory Ordering: www.youtube.com/watch?v=rMG…
-
spinlock: en.wikipedia.org/wiki/Spinlo…
-
spin-rs: github.com/mvdnes/spin…
-
parking lot:github.com/Amanieu/par…
-
Flume: github.com/zesterer/fl…
-
Crossbeam channel:docs.rs/crossbeam-c…