Rust 作为一门系统语言,与 C++ 相比
优点:
缺点:
所有权是用来管理堆上内存的一种方式,在编译阶段就可以追踪堆内存的分配和释放,不会对程序的运行期造成任何性能上的损失。
所有权规则:
借用/引用
生命周期,是引用的有效作用域。是为了避免悬垂引用而引入的,即数据已经被释放了,但引用还被使用。即被引用者的生命周期必须要比引用长。
语法:
含义:如果某个引用被标注了生命周期 'a,是告诉编译器该引用的作用域至少能持续 'a 这么久。注意,生命周期标注并不会改变任何引用的实际作用域。
一个生命周期标注,它自身并不具有什么意义,因为生命周期的作用就是告诉编译器多个引用之间的关系。
在大多数时候,无需手动声明生命周期,因为编译器可以自动进行推导:
在 Rust 中有一个非常特殊的生命周期 'static,拥有该生命周期的引用可以和整个程序活得一样久。
生命周期约束:
实现了 Deref 和 Drop trait,即为智能指针。
类似 C++ 中的 unique_ptr,是独占指针。对象的所有权可以从一个独占指针转移到另一个指针,其转移方式为:对象始终只能有一个指针作为其所有者。当独占指针离开其作用域或将要拥有不同的对象时,它会自动释放自己所管理的对象。
使用场景:
类似 C++ 中的 shared_ptr,是共享指针。共享指针将记录有多少个指针共同享有某个对象的所有权。当有更多指针被设置为指向该对象时,引用计数随之增加;当指针和对象分离时,则引用计数也相应减少。当引用计数降低至 0 时,该对象被删除。
Rc 是引用计数(reference counting)的缩写。
Rc 适用于单线程,Arc 适用于多线程,它的内部计数器是多线程安全的。
每次调用 Rc/Arc 的 clone() 时,strong_count 会加 1,当离开作用域时,Drop trait 的实现会让 strong_count 自动减 1。
Weak 是为了避免循环引用而引入的,调用其 clone() 时,strong_count 不会加 1,而是对 weak_count 加 1。
Rc/Arc 是不可变引用,无法修改它指向的值,只能进行读取,如果要修改,需要配合内部可变性 RefCell 或互斥锁 Mutex。Rc<T>/RefCell<T>用于单线程内部可变性, Arc<T>/Mutext<T>用于多线程内部可变性。
Cell 和 RefCell 用于内部可变性,可以在拥有不可变引用的同时修改内部数据。
Cell 和 RefCell 在功能上相同,区别在于 Cell 针对的是实现了 Copy 特征的值类型,它并非提供内部值的引用,而是把值拷贝进和拷贝出 Cell<T>。在实际开发中,Cell 使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 RefCell 来达成目的。
对于引用和 Box<T>,借用规则的不可变性作用于编译时。对于 RefCell<T>,这些不可变性作用于运行时。
当创建不可变和可变引用时,分别使用 & 和 &mut 语法。对于 RefCell<T> 来说,则是 borrow 和 borrow_mut 方法,这属于 RefCell<T> 安全 API 的一部分。RefCell<T> 记录当前有多少个活动的 Ref<T> 和 RefMut<T> 智能指针。像编译时借用规则一样,RefCell<T> 在任何时候只允许有多个不可变借用或一个可变借用。如果违法,会在运行时出现 panic。
由于操作系统提供了创建线程的 API,因此部分语言会直接调用该 API 来创建线程,因此最终程序内的线程数和该程序占用的操作系统线程数相等,称之为 1:1 线程模型。如果愿意牺牲一些性能来换取更精确的线程控制以及更小的线程上下文切换成本,那么可以选择 Rust 中的 M:N 模型,这些模型由三方库提供了实现,例如 tokio。
在线程闭包中使用 move
多发送者,单接收者 std::sync::mpsc
mpsc::channel();
mpsc::sync_channel(0);
mpsc::sync_channel(10);
。异步消息虽然能非常高效且不会造成发送线程的阻塞,但是存在消息未及时消费,最终内存过大的问题。在实际项目中,可以考虑使用一个带缓冲值的同步通道来避免这种风险。多发送者,多接收者
互斥锁 Mutex
读写锁 RwLock
解决资源访问顺序的问题。它经常和 Mutex 一起使用,可以让线程挂起,直到某个条件发生后再继续执行。
精准的控制当前正在运行的任务最大数量。
原子类型是无锁类型,但是无锁不代表无需等待,因为原子类型内部使用了 CAS 循环。
内存顺序是指 CPU 在访问内存时的顺序,该顺序可能受以下因素的影响:
限定内存顺序的 5 个规则
内存顺序的选择
使用场景
首先简单性上 Mutex 完胜,因为使用 RwLock 需要操心几个问题:
再来简单总结下两者的使用场景:
需要注意的是,RwLock 虽然看上去貌似提供了高并发读取的能力,但这个不能说明它的性能比 Mutex 高,事实上 Mutex 性能要好不少,后者唯一的问题也仅仅在于不能并发读取。
一个常见的、错误的使用 RwLock 的场景就是使用 HashMap 进行简单读写,因为 HashMap 的读和写都非常快,RwLock 的复杂实现和相对低的性能反而会导致整体性能的降低,因此一般来说更适合使用 Mutex。
总之,如果使用 RwLock 要确保满足以下两个条件:并发读,且需要对读到的资源进行"长时间"的操作,HashMap 也许满足了并发读的需求,但是往往并不能满足后者:"长时间"的操作。
Copy
Clone
实现 Deref 后的智能指针结构体,就可以像普通引用一样,通过 * 进行解引用。
Drop 允许指定智能指针超出作用域后自动执行的代码,例如做一些数据清除等收尾工作。
对智能指针 Box 进行解引用时,实际上 Rust 为我们调用了方法 (p.deref())。首先调用 deref() 返回值的常规引用,然后通过 对常规引用进行解引用,最终获取到目标值。如果 deref() 返回的是值而不是引用,*T 会拿走智能指针中包含的值,转移所有权。
Deref 会进行隐式转换,例如 &String 会自动转换为 &str。
Rust 还支持将一个可变的引用转换成另一个可变的引用以及将一个可变引用转换成不可变的引用。
{} 和 {:?} 都是占位符:
大部分类型都实现了 Debug,但实现了 Display 的 Rust 类型并没有那么多,往往需要我们自定义想要的格式化方式。
实现 Send 的类型可以在线程间安全的传递其所有权。
实现 Sync 的类型可以在线程间安全的共享(通过引用)。
这里还有一个潜在的依赖:一个类型要在线程间安全的共享的前提是,指向它的引用必须能在线程间传递。因为如果引用都不能被传递,就无法在多个线程间使用引用去访问同一个数据了。
由上可知,若类型 T 的引用 &T 是 Send,则 T 是 Sync。
在 Rust 中,几乎所有类型都默认实现了 Send 和 Sync,而且由于这两个特征都是可自动派生的特征(通过derive派生),意味着一个复合类型(例如结构体), 只要它内部的所有成员都实现了 Send 或者 Sync,那么它就自动实现了 Send 或 Sync。只要复合类型中有一个成员不是 Send 或 Sync,那么该复合类型也就不是 Send 或 Sync。
可以为自定义类型实现 Send 和 Sync,但是需要 unsafe 代码块。
可以为部分 Rust 中的类型实现 Send、Sync,但是需要使用 newtype。
Future 是异步函数的返回值和被执行的关键。
简化版的 Future:
#![allow(unused)]
fn main() {
trait SimpleFuture {
type Output;
fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}
enum Poll<T> {
Ready(T),
Pending,
}
}
Future 需要被执行器 poll(轮询)后才能运行。
若在当前 poll 中, Future 可以被完成,则会返回 Poll::Ready(result) ,反之则返回 Poll::Pending, 并且安排一个 wake 函数:当未来 Future 准备好进一步执行时,该函数会被调用,然后管理该 Future 的执行器会再次调用 poll 方法,此时 Future 就可以继续执行了。
Future 模型允许将多个异步操作组合在一起,同时还无需任何内存分配。
实际的 Future:
#![allow(unused)]
fn main() {
trait Future {
type Output;
fn poll(
// 首先值得注意的地方是,`self`的类型从`&mut self`变成了`Pin<&mut Self>`:
self: Pin<&mut Self>,
// 其次将`wake: fn()` 修改为 `cx: &mut Context<'_>`:
cx: &mut Context<'_>,
) -> Poll<Self::Output>;
}
}
Pin:见 7.3。
Context:包含 wake() 和 wake() 携带的数据。
Rust 的 Future 是惰性的,在 async 函数中使用 .await 来调用另一个 async 函数,但是这个只能解决 async 内部的问题,最外层的 async 函数需要 executor 来运行。
Executor:包含 task_receiver,从一个任务通道(channel)中拉取 Task,然后运行它们。
Spawner:包含 task_sender,创建新的 Task 然后将它发送到任务通道(channel)中。
Task:包含 Future 和 task_sender,它可以调度自己(将自己放入任务通道中),然后等待 Executor 去poll
。
Executor 在 poll 一个 Task 之前,会先由 Waker 将该任务放入任务通道(channel)中。
创建 Waker 的最简单的方式就是让 Task 实现 ArcWake trait。
当 Task 实现了 ArcWake trait 后,Executor 在调用其 wake() 对其唤醒后会将复制一份所有权(Arc),然后将其发送到任务通道(channel)中。最后 Executor 将从通道中获取任务,然后进行 poll 执行。
主要是为了避免自引用类型地址改变后造成的错误。
自引用类型:自己一个成员指向自己的另一个成员。例如:
struct SelfRef {
value: String,
pointer_to_value: *mut String,
}
pointer_to_value 指向了 value。假如 value 发生了移动,而 pointer_to_value 依然指向之前的地址,就会导致 bug。如果能将 SelfRef 在内存中固定到一个位置,就可以避免这种问题的发生,也就可以安全的创建上面这种引用类型。
Pin 是一个结构体:
pub struct Pin<P> {
pointer: P,
}
它包裹一个指针,并且能确保该指针指向的数据不会被移动,例如 Pin<&mut T> , Pin<&T> , Pin<Box<T>> ,都能确保 T 不会被移动。
Unpin 是一个 trait,它表明一个类型可以随意被移动。
可以被 Pin 住的值必须实现 !Unpin trait。
如果类型实现了 Unpin trait,还是可以 Pin 的,只是没有效果而已。
可以通过以下方法为自己的类型添加 !Unpin 约束:
可以将值固定到栈上,也可以固定到堆上
当固定类型T: !Unpin时,你需要保证数据从被固定到被 drop 这段时期内,其内存不会变得非法或者被重用。
Rust 语言圣经. https://course.rs/about-book.html
Rust 程序设计语言. https://kaisery.github.io/trpl-zh-cn/foreword.html
带你了解 Rust 中的 move, copy, clone. https://rustcc.cn/article?id=7916f651-f20a-4356-848b-95268036ccc1
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。