Rust标准库学习
Rust标准库学习
- 1: Rust标准库介绍
- 2: Rust标准库中的集合
- 2.1: Rust标准库中的集合介绍
- 3: Rust标准库中的同步
- 3.1: Rust标准库中的同步介绍
- 3.2: Rust标准库中的原子类型
- 3.3: Rust标准库中的原子引用计数(Arc)
- 3.4: Rust标准库中的屏障(Barrier)
- 3.5: Rust标准库中的条件变量(Condvar)
- 3.6: Rust标准库中的多生产者单消费者队列(mpsc)
- 3.7: Rust标准库中的互斥(Mutex)
- 3.8: Rust标准库中的一次性(Once)
- 3.9: Rust标准库中的读写锁(RwLock)
- 4: Rust标准库中的异步
- 4.1: Rust标准库中的异步编程
- 5: Rust标准库中的Thread
- 5.1: Rust标准库中的Thread介绍
- 6: Rust标准库中的Boxed
- 6.1: Rust标准库中的Boxed介绍
- 7: Rust标准库中的借用(borrow)
- 7.1: Rust标准库中的借用介绍
- 8: Rust标准库中的cell
- 8.1: Rust标准库中的cell介绍
- 9: Rust标准库中的克隆(clone)
- 9.1: Rust标准库中的克隆介绍
- 10: Rust标准库中的结果(result)
- 10.1: Rust标准库中的结果(result)介绍
- 11: Rust标准库中的标记(Marker)模块
- 11.1: Sized trait
- 11.1.1: Sized trait的std文档
- 11.1.2: Sized trait的源码
- 11.1.3: [Rust编程之道笔记]Sided trait
- 11.2: Unsized trait
- 11.2.1: Unsize trait的std文档
- 11.2.2: Unsize trait的源码
- 11.3: Copy trait
- 11.3.1: Copy trait的std文档
- 11.3.2: Copy trait的源码
- 11.3.3: [Rust编程之道笔记]Copy trait
- 11.4: Send trait
- 11.4.1: Send trait的源码
- 11.4.2: Send trait的std文档
- 11.4.3: [Rust编程之道笔记]Send trait
- 11.4.4: send和sync
- 11.5: Sync trait
- 11.5.1: Sync trait的std文档
- 11.5.2: Sync trait的源码
- 11.6: Unpin trait
- 11.6.1: Unsize trait的std文档
- 11.6.2: Unpin trait的源码
1 - Rust标准库介绍
介绍
https://doc.rust-lang.org/std/
Rust语言标准库是让Rust语言开发的软件具备可移植性的基础,它是一组最小的、经过实战检验的共享抽象,适用于更广泛的Rust生态系统。它提供了核心类型,如Vec
默认情况下,所有Rust crate都可以使用std。因此,标准库可以在 use 语句中通过路径 std 访问标准库,如 std:::env
。
标准库的内容有:
Async std中文文档
https://learnku.com/docs/rust-async-std/translation-notes/7132
sync:
- Arc
- Mutex
collections:
- BTreeMap
- HashMap
其他内容:
#[derive(Debug, Clone)]
2 - Rust标准库中的集合
2.1 - Rust标准库中的集合介绍
https://doc.rust-lang.org/std/collections/index.html
Rust的标准集合库提供了最常见的通用编程数据结构的高效实现。通过使用标准的实现,两个库之间的通信应该可以不需要进行大量的数据转换。
说白了:你可能只需要使用Vec或HashMap就可以了。这两个集合涵盖了大多数通用数据存储和处理的用例。它们在做它们所做的事情上表现得异常出色。标准库中的所有其他集合都有特定的用例,在这些用例中,它们是最佳的选择,但相比之下,这些用例都是小众的。即使Vec和HashMap在技术上是次优的时候,它们也可能是一个足够好的选择,可以入手。
Rust的集合可以分为四大类。
- Sequences: Vec, VecDeque, LinkedList
- Maps: HashMap, BTreeMap
- Sets: HashSet, BTreeSet
- Misc: BinaryHeap
什么时候应该使用哪个系列?
这些都是相当高层次的快速分解,说明了什么时候应该考虑每个集合。关于每个系列的优点和缺点的详细讨论可以在它们自己的文档页面上找到。
在以下情况下使用 Vec
- 想收集项目,以便以后处理或发送到其他地方,而不关心实际存储的值的任何属性。
- 想要一个特定顺序的元素序列,并且只附加到(或接近)末尾
- 想要一个堆栈
- 想要一个可调整的数组
- 想要一个堆分配的数组
在以下情况下使用 VecDeque
- 想要一个支持在序列两端有效插入的Vec
- 想要一个队列
- 想要一个双端队列(deque)
在以下情况下使用 LinkedList
- 想要一个未知大小的Vec或VecDeque,并且不能容忍摊派(tolerate amortization)。
- 想有效地分拆和附加列表。
- 绝对确定你真的真的需要一个双链接列表
在以下情况下使用 HashMap
- 想把任意的键与任意的值关联起来。
- 想要一个缓存。
- 想要一个map,没有额外的功能。
在以下情况下使用BTreeMap
- 想要一个按键值排序的地图。
- 希望能够按需获得一系列的条目。
- 想知道最小或最大的键值对是什么。
- 想找到最大或最小的键值比什么东西小或大的键。
在以下情况下使用这些map中的任何一个的Set变体
- 只想记住看到过哪些键。
- 没有任何有意义的值与您的键相关联。
- 只需要一个集合。
在下列情况下使用 BinaryHeap
- 想存储一堆元素,但在任何时候只想处理 “最大的 “或 “最重要的 “一个元素
- 想要一个优先级队列
性能
选择合适的集合,需要了解每个集合擅长什么。这里我们简单地总结了不同集合在某些重要操作中的性能。要了解更多细节,请参阅每种类型的文档,并注意实际方法的名称可能与下面的表格中的某些集合不同。
在整个文档中,我们将遵循一些惯例。对于所有的操作,集合的大小用n表示,如果操作中涉及到另一个集合,则包含m个元素。有摊销成本的操作用*作为后缀。有预期成本的作业用”~“作为后缀。
所有的摊销成本是指当容量用尽时可能需要重新调整大小。如果发生调整大小,需要O(n)时间。我们的集合从来不会自动缩小,所以移除操作不会摊销。在足够大的一系列操作中,每次操作的平均成本将确定地等于给定成本。
只有HashMap有预期成本,这是由于散列的概率性。理论上,HashMap的性能较差是可能的,尽管可能性很小。
Sequences
get(i) | insert(i) | remove(i) | append | split_off(i) | |
---|---|---|---|---|---|
Vec |
O(1) | O(n-i)* | O(n-i) | O(m)* | O(n-i) |
VecDeque |
O(1) | O(min(i, n-i))* | O(min(i, n-i)) | O(m)* | O(min(i, n-i)) |
LinkedList |
O(min(i, n-i)) | O(min(i, n-i)) | O(min(i, n-i)) | O(1) | O(min(i, n-i)) |
需要注意的是,如果出现平局,Vec一般会比VecDeque快,而VecDeque一般会比LinkedList快。
Map
对于Sets,所有的操作都和等价的Map操作的有相同的开销。
get | insert | remove | predecessor | append | |
---|---|---|---|---|---|
HashMap |
O(1)~ | O(1)~* | O(1)~ | N/A | N/A |
BTreeMap |
O(log n) | O(log n) | O(log n) | O(log n) | O(n+m) |
正确和有效地使用集合的方法
当然,知道了哪种集合对工作是正确的选择,并不能马上就能正确使用它。以下是一些关于标准集合的高效和正确使用的快速提示。如果你对如何使用特定的集合有兴趣,请参考其文档中的详细讨论和代码示例。
容量管理
许多集合提供了多个构造器和方法,这些构造器和方法都是指 “容量”。这些集合一般是建立在一个数组之上。在最理想的情况下,这个数组的大小应该是完全正确的,只容纳存储在集合中的元素,但对于集合来说,这样做的效率会非常低。如果后备数组在任何时候都是完全正确的大小,那么每次插入一个元素时,集合就必须增长数组来容纳它。由于大多数计算机的内存分配和管理方式,这几乎肯定需要分配一个全新的数组,并将旧数组中的每一个元素复制到新数组中。你能看到,这对每一次操作来说都不是很有效。
因此,大多数集合使用的是一种摊销分配策略。它们一般会让自己有相当数量的未被占用的空间,这样它们只需要偶尔增长。当它们增长的时候,它们会分配一个大得多的数组来移动元素,这样就需要一段时间才能再增长一次。虽然这种策略在一般情况下是很好的,但如果集合永远不需要重新调整其支持的数组的大小就更好了。不幸的是,集合本身并没有足够的信息来做这件事。因此,要靠我们这些程序员来给它提示。
任何 with_capacity 构造函数都会指示集合为指定的元素数量分配足够的空间。理想的情况下,这将是为这么多的元素分配足够的空间,但一些实现细节可能会阻止这一点。详情请参阅集合的特定文档。一般来说,当你知道要插入多少个元素时,使用 with_capacity,或者至少对这个数字有一个合理的上限。
当预计会有大量的元素涌入时,可以使用 reserve 系列方法来提示集合应该为即将到来的项目留出多少空间。就像用with_capacity 一样,这些方法的精确行为将取决于所关注的集合。
为了获得最佳性能,集合通常会避免自己缩减。如果你认为一个集合很快就不会再包含任何元素了,或者只是真的需要内存,那么 shrink_to_fit 方法就会提示集合将备份数组缩小到能够容纳元素的最小尺寸。
最后,如果你想知道集合的实际容量是多少,大多数集合提供了 capacity 方法来查询这个信息。这可以用于调试目的,或者与 reserve 方法一起使用。
迭代器
迭代器是一个强大而稳健的机制,在整个Rust的标准库中使用。迭代器以一种通用、安全、高效和方便的方式提供了值的序列。迭代器的内容通常是延迟地进行评估,因此只有实际需要的值才会被实际产生,不需要进行分配来临时存储。迭代器主要是使用 for 循环消费,尽管许多函数在需要集合或值序列的时候也会采取迭代器。
所有的标准集合都提供了多个迭代器,用于执行对其内容的批量操作。几乎每个集合都应该提供的三个主要迭代器是 iter、iter_mut 和 into_iter。其中一些在集合中没有提供,如果提供这些迭代器是不健全或不合理的。
iter以最 “自然 “的顺序为集合的所有内容提供了一个不变的引用迭代器。对于像Vec这样的序列集合,这意味着从0开始,项目将按索引的递增顺序产生;对于像BTreeMap这样的有序集合,这意味着项目将按排序顺序产生。对于像HashMap这样的无序集合,项目将以内部表示最方便的顺序产生。这对于游历集合的所有内容是非常好的。
let vec = vec![1, 2, 3, 4];
for x in vec.iter() {
println!("vec contained {}", x);
}
iter_mut 提供了一个可变引用的迭代器,其顺序与 iter 相同。这对于改变集合中的所有内容是非常好的。
let mut vec = vec![1, 2, 3, 4];
for x in vec.iter_mut() {
*x += 1;
}
into_iter将实际的集合按值转换为一个迭代器。当集合本身不再需要,而其他地方需要值的时候,这个方法就很好用。配合使用extend和 into_iter是将一个集合的内容转移到另一个集合中的主要方式。extend 自动调用 into_iter,并获取任何 T:
IntoIterator
。在迭代器本身上调用 collect 也是将一个集合转换为另一个集合的好方法。这两种方法都应该在内部使用上一节中讨论过的容量管理工具来尽可能高效地完成这一任务。
let mut vec1 = vec![1, 2, 3, 4];
let vec2 = vec![10, 20, 30, 40];
vec1.extend(vec2);
use std::collections::VecDeque;
let vec = vec![1, 2, 3, 4];
let buf: VecDeque<_> = vec.into_iter().collect();
迭代器还提供了一系列的 adapter 方法,用于为序列执行常见的操作。在这些 adapter 方法中,有很多函数式的喜好,比如map、fold、skip和take。对于集合来说,特别感兴趣的是 rev 适配器,它可以对任何支持这种操作的迭代器进行可逆迭代。大多数集合都提供了可逆迭代器,作为对其进行反向迭代的方式。
let vec = vec![1, 2, 3, 4];
for x in vec.iter().rev() {
println!("vec contained {}", x);
}
其他一些集合方法也返回迭代器来产生结果序列,但避免分配整个集合来存储结果。这提供了最大的灵活性,因为如果需要的话,可以调用 collect 或 extend 来将序列 “管道 “到任何集合中。否则,这个序列可以用for循环来循环。迭代器也可以在部分使用后丢弃,防止计算未使用的项。
Entry
Entry API的目的是提供一个高效的机制,以有条件地操作 map 的内容,是否存在密钥为条件。其主要的动机用例是提供有效的累积器map。例如,如果一个人希望统计每个key被看到的次数,他们必须执行一些条件逻辑来判断这个键是否是第一次被看到。通常情况下,这将需要在查找之后再进行插入,有效地重复了每次插入时的搜索工作。
当用户调用map.entry(&key)时,地图将搜索该键,然后产生一个Entry enum的变体。
如果产生了一个Vacant(entry),则表示没有找到该key。在这种情况下,唯一有效的操作就是在条目中插入一个值。完成后,空缺的条目被消费掉,并转换为插入的值的可变引用。这样可以在搜索本身的生命周期之外对值进行进一步的操作。如果需要对该值进行复杂的逻辑处理,不管该值是否刚刚被插入,这一点是非常有用的。
如果产生了一个Occupied(entry),那么就表示已经找到了键。在这种情况下,用户有几个选项:他们可以获取、插入或删除被占用的条目的值。此外,他们可以将被占用的条目转换为其值的可变引用,为空缺插入情况提供对称性。
这里主要用两种使用 entry 的方式。首先是一个简单的例子,在这个例子中,对值进行的逻辑是琐碎的。
计算字符串中每个字符出现的次数
use std::collections::btree_map::BTreeMap;
let mut count = BTreeMap::new();
let message = "she sells sea shells by the sea shore";
for c in message.chars() {
*count.entry(c).or_insert(0) += 1;
}
assert_eq!(count.get(&'s'), Some(&8));
println!("Number of occurrences of each character");
for (char, count) in &count {
println!("{}: {}", char, count);
}
当值上要执行的逻辑比较复杂时,我们可以简单的使用entry API来保证值的初始化,然后再执行后面的逻辑。
追踪顾客在酒吧的醉酒情况
use std::collections::btree_map::BTreeMap;
// A client of the bar. They have a blood alcohol level.
struct Person { blood_alcohol: f32 }
// All the orders made to the bar, by client ID.
let orders = vec![1, 2, 1, 2, 3, 4, 1, 2, 2, 3, 4, 1, 1, 1];
// Our clients.
let mut blood_alcohol = BTreeMap::new();
for id in orders {
// If this is the first time we've seen this customer, initialize them
// with no blood alcohol. Otherwise, just retrieve them.
let person = blood_alcohol.entry(id).or_insert(Person { blood_alcohol: 0.0 });
// Reduce their blood alcohol level. It takes time to order and drink a beer!
person.blood_alcohol *= 0.9;
// Check if they're sober enough to have another beer.
if person.blood_alcohol > 0.3 {
// Too drunk... for now.
println!("Sorry {}, I have to cut you off", id);
} else {
// Have another!
person.blood_alcohol += 0.1;
}
}
插入和复合键
如果我们有一个比较复杂的键,调用插入将不会更新该键的值。比如说。
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::hash::{Hash, Hasher};
#[derive(Debug)]
struct Foo {
a: u32,
b: &'static str,
}
// we will compare `Foo`s by their `a` value only.
impl PartialEq for Foo {
fn eq(&self, other: &Self) -> bool { self.a == other.a }
}
impl Eq for Foo {}
// we will hash `Foo`s by their `a` value only.
impl Hash for Foo {
fn hash<H: Hasher>(&self, h: &mut H) { self.a.hash(h); }
}
impl PartialOrd for Foo {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { self.a.partial_cmp(&other.a) }
}
impl Ord for Foo {
fn cmp(&self, other: &Self) -> Ordering { self.a.cmp(&other.a) }
}
let mut map = BTreeMap::new();
map.insert(Foo { a: 1, b: "baz" }, 99);
// We already have a Foo with an a of 1, so this will be updating the value.
map.insert(Foo { a: 1, b: "xyz" }, 100);
// The value has been updated...
assert_eq!(map.values().next().unwrap(), &100);
// ...but the key hasn't changed. b is still "baz", not "xyz".
assert_eq!(map.keys().next().unwrap().b, "baz");
3 - Rust标准库中的同步
3.1 - Rust标准库中的同步介绍
https://doc.rust-lang.org/std/sync/index.html
有用的同步原语。
同步化的需要
从概念上讲,Rust程序是指在计算机上执行的一系列操作。程序中发生事件的时间线与代码中的操作顺序是一致的。
考虑一下下面的代码,在一些全局静态变量上进行操作。
static mut A: u32 = 0;
static mut B: u32 = 0;
static mut C: u32 = 0;
fn main() {
unsafe {
A = 3;
B = 4;
A = A + B;
C = B;
println!("{} {} {}", A, B, C);
C = A;
}
}
看起来好像是改变了一些存储在内存中的变量,进行了一次加法,结果存储在A中,变量C被修改了两次。
当只涉及到一个线程时,结果和预期的一样:7 4 4 4行被打印出来。
至于幕后会发生什么,当启用优化后,最终生成的机器代码可能会和代码中的样子大不相同。
-
第一个存储到C的存储可能会被移到A或B之前,就像我们写的C = 4; A = 3; B = 4一样。
-
A+B的赋值可能会被删除,因为和可以存储在一个临时位置,直到它被打印出来,而全局变量永远不会被更新。
-
最终的结果可以通过在编译时查看代码来确定,所以恒定折叠可能会把整个代码块变成一个简单的println!(“7 4 4 4”)。
编译器可以执行这些优化的任意组合,只要最终优化后的代码在执行时,产生的结果与没有优化的代码相同。
由于现代计算机的并发性,对程序执行顺序的假设往往是错误的。对全局变量的访问可能会导致非确定性的结果,即使是在编译器优化被禁用的情况下,仍然有可能引入同步bug。
需要注意的是,由于Rust的安全保证,访问全局(静态)变量需要不安全的代码,假设我们没有使用本模块中的任何同步基元,那么访问全局(静态)变量就需要不安全的代码。
越级执行
由于各种原因,指令的执行顺序可能与我们定义的顺序不同。
-
编译器重排序:如果编译器可以在更早的时候发出指令,它就会尝试这样做。例如,它可能会在代码块的顶部吊起内存负载,这样CPU就可以开始从内存中预取值。
-
在单线程场景中,当编写信号处理程序或某些类型的低级代码时,这可能会引起问题。使用编译器栅栏来防止这种重新排序。
-
单个处理器执行指令的顺序失序。现代CPU能够超尺度执行,即可能同时执行多条指令,即使机器代码描述的是一个顺序过程。
这种重排序是由CPU透明地处理的。
- 一个多处理器系统同时执行多个硬件线程的情况。在多线程场景中,可以使用两种基元来处理同步问题。
- 内存栅栏,确保内存访问以正确的顺序被其他CPU看到。
- 二是原子操作,以确保同时访问同一内存位置不会导致非定义行为。
更高层次的同步对象
大多数低级的同步原语都相当容易出错,使用起来也不方便,这也是为什么标准库也暴露了一些更高级别的同步对象的原因。
这些抽象对象可以从低级原语中构建出来。为了提高效率,标准库中的同步对象通常是在操作系统内核的帮助下实现的,当线程在获取锁时被阻塞时,内核能够对线程进行重新安排。
下面是可用的同步对象的概述:
-
Arc:原子引用计数(Atomically Reference-Counted)指针,可以在多线程环境下使用,以延长某些数据的使用寿命,直到所有线程都使用完为止。
-
Barrier:屏障。确保多个线程相互等待对方到达程序中的某个点,然后再一起继续执行。
-
Condvar:条件变量,提供在等待事件发生时阻止一个线程的能力。
-
mpsc:多生产者,单消费(Multi-producer, single-consumer)队列,用于基于消息的通信。可以提供轻量级的线程间同步机制,代价是增加一些额外的内存。
-
互斥机制(Mutex)。互斥机制,确保每次最多只有一个线程能够访问一些数据。
-
Once:用于全局变量的线程安全的一次性初始化。
-
RwLock:用于全局变量的初始化。提供了一个相互排斥机制,允许多个读同时访问,同时一次只允许一个写。在某些情况下,这可能比mutex更有效。
3.2 - Rust标准库中的原子类型
https://doc.rust-lang.org/std/sync/atomic/index.html
原子类型
原子类型提供了线程之间的原始共享内存通信,也是其他并发类型的构件。
这个模块定义了一些精选的原始类型的原子版本,包括AtomicBool、AtomicIsize、AtomicUsize、AtomicI8、AtomicU16等。Atomic类型呈现的操作,正确使用时,可以在线程之间同步更新。
每个方法都取 Ordering
,它代表该操作的内存屏障强度。这些顺序与C++20的原子顺序相同。更多信息,请参见nomicon。
原子变量在线程之间共享是安全的(它们实现了Sync),但它们本身并没有提供共享的机制,遵循Rust的线程模型。共享原子变量最常见的方式是将其放入Arc(原子引用计数的共享指针)。
原子类型可以存储在静态变量中,使用像 AtomicBool:::new 这样的常量初始化器进行初始化。原子静态常量经常被用于延迟的全局初始化。
可移植性
这个模块中的所有原子类型都保证是无锁的,只要可用。这意味着它们不会在内部使用全局mutex。原子类型和操作不保证是无等待的。这意味着像 fetch_or 这样的操作可以通过比较和交换循环(compare-and-swap)来实现。
原子操作可以在指令层用较大尺寸的原子类型来实现。例如有些平台使用4字节的原子指令来实现AtomicI8。注意,这种模拟应该不会对代码的正确性产生影响,只是需要注意的地方。
这个模块中的原子类型可能并不是所有平台都能使用。但是,这里的原子类型都是广泛可用的,一般来说,可以依赖现有的原子类型。一些值得注意的例外是。
-
具有32位指针的PowerPC和MIPS平台没有AtomicU64或AtomicI64类型。
-
非Linux的ARM平台,如armv5te,根本就没有AtomicU64或AtomicI64类型。
-
采用thumbv6m的ARM目标完全没有原子操作。
需要注意的是,未来可能会加入同样不支持某些原子操作的平台。最大限度的可移植代码要注意使用哪些原子类型。一般来说,AtomicUsize和AtomicIsize是最可移植的,但即便如此,也不是到处都有。作为参考,std库需要指针大小的原子类型,虽然core不需要。
目前你需要使用#[[cfg(target_arch)]]主要是为了有条件地在代码中编译原子体。还有一个不稳定的#[cfg(target_has_atomic)]也是不稳定的,将来可能会稳定下来。
例子
一个简单的自旋锁:
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
fn main() {
let spinlock = Arc::new(AtomicUsize::new(1));
let spinlock_clone = spinlock.clone();
let thread = thread::spawn(move|| {
spinlock_clone.store(0, Ordering::SeqCst);
});
// Wait for the other thread to release the lock
while spinlock.load(Ordering::SeqCst) != 0 {}
if let Err(panic) = thread.join() {
println!("Thread had an error: {:?}", panic);
}
}
保持全局的活跃线程数:
use std::sync::atomic::{AtomicUsize, Ordering};
static GLOBAL_THREAD_COUNT: AtomicUsize = AtomicUsize::new(0);
let old_thread_count = GLOBAL_THREAD_COUNT.fetch_add(1, Ordering::SeqCst);
println!("live threads: {}", old_thread_count + 1);
Ordering枚举
https://doc.rust-lang.org/std/sync/atomic/enum.Ordering.html
pub enum Ordering {
Relaxed,
Release,
Acquire,
AcqRel,
SeqCst,
}
内存顺序指定了原子操作同步内存的方式。在其最弱的Relaxed中,只有操作直接接触到的内存才会被同步。另一方面,SeqCst操作的存储-加载对在同步其他内存的同时,还额外保留了所有线程中此类操作的总顺序。
Rust的内存顺序与C++20的内存顺序相同。
更多的信息请参见nomicon。
-
Relaxed
没有排序约束,只有原子操作。
对应于C++20中的 memory_order_relaxed。
-
Release
当与存储结合在一起时,所有之前的操作都会在加载该值的任何 Acquire(或更强的)排序之前变得有序。特别是,所有之前的所有写操作都会对执行该值的Acquire(或更强)加载的线程可见。
请注意,对结合了加载和存储的操作使用此命令会导致 Relaxed 加载操作!
这个命令只适用于可以执行存储的操作。
对应于C++20中的 memory_order_release。
-
Acquire
当与加载结合在一起时,如果加载的值是由具有Release(或更强的)排序的存储操作写入的,那么后续的所有操作都会在该存储之后成为排序。特别是,所有后续的加载操作都会看到在存储操作之前写入的数据。
请注意,对一个结合了加载和存储的操作使用这种排序会导致一个Relaxed存储操作!
这个命令只适用于可以执行加载的操作。
对应于C++20中的memory_order_acquire。
-
AcqRel
同时具有Acquire和Release的效果。对于负载,它使用的是Acquire命令。对于存储,它使用的是Release命令。
注意,在compare_and_swap的情况下,有可能操作最终没有执行任何存储,因此它只执行Acquire命令。然而,AcqRel永远不会执行Relaxed访问。
这种命令只适用于同时执行加载和存储的操作。
对应于C++20中的memory_order_acq_rel。
-
SeqCst
类似于Acquire/Release/AcqRel(分别用于加载、存储和加载与存储操作),额外保证了所有线程以相同的顺序查看所有顺序一致的操作。
对应于C++20中的memory_order_seq_cst。
compiler_fence函数
pub fn compiler_fence(order: Ordering)
编译器的内存围栏。
compiler_fence不发出任何机器代码,但限制了编译器允许做的内存重排序的种类。具体来说,根据给定的 Ordering 语义,编译器可能会被禁止从调用 compiler_fence 之前或之后的读或写移动到调用 compiler_fence 的另一端。注意,这并不妨碍硬件进行这种重新排序。这在单线程、执行上下文中不是问题,但当其他线程可能同时修改内存时,需要更强的同步基元(如fence)。
不同的排序语义所阻止的重排序是。
- 用SeqCst,不允许跨这个点的读和写的重新排序。
- 使用Release,前面的读和写不能越过后续的写。
- 使用Acquire,后续的读和写不能在前面的读之前移动。
- 使用AcqRel,以上两条规则都会被执行。
compiler_fence通常只对防止线程与自己赛跑有用。也就是说,如果一个给定的线程正在执行一段代码,然后被中断,并开始执行其他地方的代码(同时仍然在同一个线程中,并且在概念上仍然在同一个内核上)。在传统程序中,这种情况只有在注册了信号处理程序后才会出现。在更多的底层代码中,在处理中断时、实现绿色线程预置时等也会出现这种情况。鼓励好奇的读者阅读Linux内核中关于内存障碍的讨论。
例子:
如果没有 compiler_fence,下面的代码中的assert_eq!中的assert_eq!不能保证成功,尽管一切都发生在一个线程中。要了解原因,请记住,编译器可以自由地将存储空间调换成import_variable和is_read,因为它们都是Ording:::Relaxed。如果它这样做了,并且信号处理程序在IS_READY被更新后立即被调用,那么信号处理程序将看到IS_READY=1,但IMPORTANT_VARIABLE=0。 使用compiler_fence可以纠正这种情况。
use std::sync::atomic::{AtomicBool, AtomicUsize};
use std::sync::atomic::Ordering;
use std::sync::atomic::compiler_fence;
static IMPORTANT_VARIABLE: AtomicUsize = AtomicUsize::new(0);
static IS_READY: AtomicBool = AtomicBool::new(false);
fn main() {
IMPORTANT_VARIABLE.store(42, Ordering::Relaxed);
// prevent earlier writes from being moved beyond this point
compiler_fence(Ordering::Release);
IS_READY.store(true, Ordering::Relaxed);
}
fn signal_handler() {
if IS_READY.load(Ordering::Relaxed) {
assert_eq!(IMPORTANT_VARIABLE.load(Ordering::Relaxed), 42);
}
}
fence函数
https://doc.rust-lang.org/std/sync/atomic/fn.fence.html
pub fn fence(order: Ordering)
一个原子栅栏。
根据指定的顺序,栅栏可以防止编译器和CPU围绕着它重新排序某些类型的内存操作。这就在它和其他线程中的原子操作或栅栏之间建立了同步关系。
一个具有(至少)释放排序语义的栅栏’A’与具有(至少)获得语义的栅栏’B’同步,如果且仅当存在操作X和Y,都在某些原子对象’M’上操作,那么A在X之前被排序,Y在B之前被同步,而Y观察到M的变化。
Thread 1 Thread 2
fence(Release); A --------------
x.store(3, Relaxed); X --------- |
| |
| |
-------------> Y if x.load(Relaxed) == 3 {
|-------> B fence(Acquire);
...
}
具有Release或Acquire语义的原子操作也可以与栅栏同步。
一个具有SeqCst排序的栅栏,除了具有Acquire和Release语义外,还可以参与其他SeqCst操作和/或栅栏的全局程序排序。
接受Acquire、Release、AcqRel和SeqCst命令。
例子:
use std::sync::atomic::AtomicBool;
use std::sync::atomic::fence;
use std::sync::atomic::Ordering;
// 基于自旋锁的互斥基元。
pub struct Mutex {
flag: AtomicBool,
}
impl Mutex {
pub fn new() -> Mutex {
Mutex {
flag: AtomicBool::new(false),
}
}
pub fn lock(&self) {
while !self.flag.compare_and_swap(false, true, Ordering::Relaxed) {}
// This fence synchronizes-with store in `unlock`.
fence(Ordering::Acquire);
}
pub fn unlock(&self) {
self.flag.store(false, Ordering::Release);
}
}
spin_loop_hint函数
https://doc.rust-lang.org/std/sync/atomic/fn.spin_loop_hint.html
pub fn spin_loop_hint()
向处理器发出信号,表示它处于忙等待的自旋循环(“spin lock”)中。
接收到自旋循环信号后,处理器可以通过节省功耗或切换超线程等方式优化自己的行为。
这个函数不同于 std::thread::yield_now,后者直接向系统的调度器输出,而spin_loop_hint不与操作系统交互。
spin_loop_hint的一个常见用例是在同步基元中实现CAS循环中的约束优化旋转。为了避免优先级反转等问题,强烈建议在有限的迭代次数后终止自旋循环,并进行适当的阻塞syscall。
注意:在不支持接收自旋循环提示的平台上,这个函数根本不做任何事情。
3.3 - Rust标准库中的原子引用计数(Arc)
https://doc.rust-lang.org/std/sync/struct.Arc.html
线程安全的引用计数指针。‘Arc’代表 “Atomically Reference Counted/原子引用计数”。
类型 Arc
Rust中的共享引用默认不允许改变,Arc也不例外:一般情况下,你无法获得Arc内部的东西的可变引用。如果你需要通过Arc进行改变,请使用Mutex、RwLock或Atomic类型。
线程安全
与 Rc
只要T实现了Send和Sync,ArcArc<RefCell<T>>
。RefCellArc<RefCell<T>
也会是Send。但这样一来,我们就有问题了。RefCell
最后,这意味着你可能需要将Arc
用 weak 打破循环
downgrade方法可以用来创建一个无主的Weak指针。Weak指针可以升级为Arc,但是如果存储在分配中的值已经被降级,则会返回None。换句话说,Weak指针不会使分配中的值保持活的,但是,它们会使分配(值的后备存储)保持活的。
Arc指针之间的循环永远不会被dealocated。基于这个原因,Weak被用来打破循环。例如,一棵树可以有从父节点到子节点的强Arc指针,而从子节点回到父节点的弱指针。
3.4 - Rust标准库中的屏障(Barrier)
https://doc.rust-lang.org/std/sync/struct.Barrier.html
Barrier/屏障可以让多个线程同步开始某些计算。
use std::sync::{Arc, Barrier};
use std::thread;
let mut handles = Vec::with_capacity(10);
let barrier = Arc::new(Barrier::new(10));
for _ in 0..10 {
let c = barrier.clone();
// The same messages will be printed together.
// You will NOT see any interleaving.
handles.push(thread::spawn(move|| {
println!("before wait");
c.wait();
println!("after wait");
}));
}
// Wait for other threads to finish.
for handle in handles {
handle.join().unwrap();
}
new 方法
use std::sync::Barrier;
let barrier = Barrier::new(10);
创建一个新的屏障,可以阻止给定数量的线程。
屏障将阻止n-1个线程调用等待,然后当第n个线程调用等待时,立即唤醒所有线程。
wait 方法
屏蔽当前线程,直到所有线程在这里会合。
Barrier在所有线程会合后可以重复使用,并且可以连续使用。
单个(任意)线程在从这个函数返回时,会收到一个 is_leader返回 true 的 BarrierWaitResult,其他所有线程都会收到is_leader返回false的结果。
3.5 - Rust标准库中的条件变量(Condvar)
https://doc.rust-lang.org/std/sync/struct.Condvar.html
条件变量
条件变量代表阻止线程的能力,使其在等待事件发生时不消耗CPU时间。条件变量通常与布尔谓词(一个条件/condition)和mutex关联。在确定线程必须阻止之前,该谓词总是在mutex内部进行验证。
这个模块中的函数将阻止当前线程的执行,并尽可能地绑定到系统提供的条件变量。注意,这个模块对系统条件变量有一个额外的限制:每个condvar在运行时只能使用一个mutex。任何试图在同一个条件变量上使用多个mutexes的行为都会导致运行时的恐慌。如果不希望这样,那么sys中的不安全基元就没有这个限制,但可能会导致未定义的行为。
use std::sync::{Arc, Mutex, Condvar};
use std::thread;
let pair = Arc::new((Mutex::new(false), Condvar::new()));
let pair2 = pair.clone();
// Inside of our lock, spawn a new thread, and then wait for it to start.
thread::spawn(move|| {
let (lock, cvar) = &*pair2;
let mut started = lock.lock().unwrap();
*started = true;
// We notify the condvar that the value has changed.
cvar.notify_one();
});
// Wait for the thread to start up.
let (lock, cvar) = &*pair;
let mut started = lock.lock().unwrap();
while !*started {
started = cvar.wait(started).unwrap();
}
wait-while方法
阻止当前线程,直到这个条件变量收到通知,并且提供的条件为false。
这个函数将原子化地解锁指定的mutex(用 guard 表示),并阻塞当前线程。这意味着,任何在mutex解锁后逻辑上发生的notify_one或notify_all的调用都是唤醒这个线程的候选函数。当这个函数调用返回时,指定的锁将被重新获得。
// Wait for the thread to start up.
let (lock, cvar) = &*pair;
// As long as the value inside the `Mutex<bool>` is `true`, we wait.
let _guard = cvar.wait_while(lock.lock().unwrap(), |pending| { *pending }).unwrap();
3.6 - Rust标准库中的多生产者单消费者队列(mpsc)
https://doc.rust-lang.org/std/sync/mpsc/index.html
多生产者、单消费者FIFO队列通信原语。
mpsc = Multiple Producer Single Consumer
该模块提供在通道(channel)上基于消息的通信,具体定义为三种类型。
- Sender
- SyncSender
- Receiver
Sender 或 SyncSender 用于向 Receiver 发送数据。这两个 sender 都是可克隆的(多生产者),这样,许多线程可以同时向一个 Receiver 发送数据(单消费者)。
这些通道(channel)有两种类型。
-
异步的、无限缓冲的通道(channel)。channel 函数将返回一个(Sender, Receiver)元组,其中所有的发送都是异步的(它们从不阻塞)。该通道在概念上有一个无限缓冲。
-
同步的、有边界的通道(channel)。sync_channel 函数将返回一个(SyncSender, Receiver) tuple,在这个函数中,等待消息的存储是一个预先分配的固定大小的缓冲区。所有的发送都将通过阻塞来同步,直到有可用的缓冲区空间。请注意,允许边界为0,这将使该通道成为一个 “会合 “通道,每个发送方都会将消息原子化地交给接收方。
断连
通道上的发送和接收操作都会返回一个结果,表示操作是否成功。一个不成功的操作通常表示一个通道的另一半通道被丢弃在相应的线程中而 “挂断”。
一旦一个通道的一半被deocallated,大多数操作就不能再继续进行,所以会返回Err。许多应用程序会继续解包这个模块返回的结果,如果其中一个线程意外死亡,就会在线程之间传播失败。
3.7 - Rust标准库中的互斥(Mutex)
https://doc.rust-lang.org/std/sync/struct.Mutex.html
用于保护共享数据的互斥原语
这个mutex将阻止等待锁可用的线程。mutex也可以被静态初始化或通过 new 构造函数创建。每个mutex都有一个 type 参数,表示它所保护的数据。这些数据只能通过 lock 和 try_lock 返回的 RAII 守护来访问,这保证了只有当mutex被锁定时,数据才会被访问。
毒化/Poisoning
这个模块中的 mutex 实现了一种叫做 " poisoning “的策略,每当持有 mutex 的线程恐慌时,就会认为 mutex 中毒。一旦 mutex 被毒化,所有其他线程都无法默认访问该数据,因为它很可能被污染了(某些不变性没有被维护)。
对于mutex来说,这意味着 lock 和 try_lock 方法会返回一个Result值,表示mutex是否被毒化。大多数使用mutex的方法都会简单地将这些结果 unwrap(),在线程之间传播恐慌,以确保不会出现可能无效的变量。
然而,被毒化的 mutex 并不会阻止对底层数据的所有访问。PoisonError 类型有一个 into_inner 方法,它将返回在成功锁定时返回的守护。这样,尽管锁被毒化了,但仍然可以访问数据。
new方法
在解锁状态下创建一个新的mutex,可以随时使用。
pub fn new(t: T) -> Mutex<T>
use std::sync::Mutex;
let mutex = Mutex::new(0);
lock方法
获取一个mutex,阻塞当前线程,直到它能够这样做。
这个函数将阻塞本地线程,直到它可以获取mutex为止。返回后,该线程是唯一一个持有锁的线程。返回一个RAII守护,允许范围化解锁。当保护罩超出范围时,mutex将被解锁。
在已经持有锁的线程中锁定一个mutex的确切行为没有说明。但是,这个函数在第二次调用时不会返回(例如,它可能会出现恐慌或死锁)。
try_lock方法
尝试获取此锁。
如果此时无法获得该锁,则返回Err。否则,将返回一个RAII护卫。当护卫被丢弃时,该锁将被解锁。
此功能不阻塞。
3.8 - Rust标准库中的一次性(Once)
https://doc.rust-lang.org/std/sync/struct.Once.html
一个同步原语,可用于运行一次性全局初始化。用于FFI或相关功能的一次性初始化。这种类型只能用 Once:::new 构造函数构造。
call_once 方法
执行初始化例程一次,并且只执行一次。如果这是 call_once 第一次被调用,给定的闭包将被执行,否则不会调用该例程。
如果当前有另一个初始化例程正在运行,该方法将阻止调用的线程。
当这个函数返回时,保证一些初始化已经运行并完成(可能不是指定的闭包)。它还保证由执行的闭包所执行的任何内存写入都能被其他线程在这时可靠地观察到(闭包和返回后执行的代码之间存在着发生前的关系)。
如果给定的闭包在同一个 Once 实例上递归调用 call_once,则没有指定确切的行为,允许的结果是恐慌或死锁。
3.9 - Rust标准库中的读写锁(RwLock)
https://doc.rust-lang.org/std/sync/struct.RwLock.html
读写锁
这种类型的锁允许在任何时间点上有多个读或最多一个写。这种锁的写部分通常允许修改底层数据(独占访问),而读部分通常允许只读访问(共享访问)。
相比之下,Mutex不区分获取该锁的读写,因此会阻止任何等待该锁可用的线程。RwLock将允许任何数量的读获取该锁,只要一个写不持有该锁。
锁的优先级策略取决于底层操作系统的实现,这种类型并不保证会使用任何特定的策略。
类型参数T代表这个锁所保护的数据。它要求T满足Send满足跨线程共享和满足Sync以便通过reader并发访问。从锁方法中返回的RAII守护实现了Deref(和写方法的DerefMut),允许访问锁的内容。
毒化
像Mutex一样,RwLock会在恐慌时中毒。但是,请注意,只有在RwLock被完全锁定时(写模式)发生恐慌时,RwLock才会中毒。如果恐慌发生在任何读卡器中,那么该锁将不会中毒。
4 - Rust标准库中的异步
4.1 - Rust标准库中的异步编程
资料
- Asynchronous Programming in Rust: Rust官方的 async book
- async-std 中文文档
5 - Rust标准库中的Thread
5.1 - Rust标准库中的Thread介绍
https://doc.rust-lang.org/std/thread/index.html
原生线程。
线程模式
一个正在执行的Rust程序由一系列原生操作系统线程组成,每个线程都有自己的堆栈和本地状态。线程可以被命名,并提供一些内置的低级同步支持。
线程之间的通信可以通过通道、Rust的消息传递类型以及其他形式的线程同步和共享内存数据结构来完成。特别是,那些被保证为线程安全的类型可以很容易地在线程之间使用原子引用计数容器Arc来共享。
Rust中的致命逻辑错误会导致线程恐慌,在这一过程中,线程会解开堆栈,运行解析器并释放拥有的资源。虽然Rust中的恐慌并不是作为 “try/catch"机制,但是,Rust中的恐慌还是可以通过catch_unwind来捕获(除非在编译时使用panic=abort)并恢复,或者用 resume_unwind 来恢复。如果 panic没有被捕获,线程就会退出,但是可以选择从不同的线程中用 join 检测到 panic。如果主线程在没有捕获到恐慌的情况下发生恐慌,应用程序将以非零退出代码退出。
当Rust程序的主线程终止时,即使其他线程还在运行,整个程序也会关闭。但是,这个模块提供了方便的设施,可以自动等待子线程的终止(即join)。
生成线程
可以使用 thread:::spawn 函数生成一个新的线程。
use std::thread;
thread::spawn(move || {
// some work here
});
在这个例子中,生成的线程是与当前线程 “分离 “的。这意味着它可以超过它的父线程(产生它的线程),除非这个父线程是主线程。
父线程也可以等待子线程的完成;调用 spawn 会产生 JoinHandle,它提供了一个用于等待的 join 方法。
use std::thread;
let child = thread::spawn(move || {
// some work here
});
// some work here
let res = child.join();
join方法返回 thread::Result ,其中包含子线程产生的最终值Ok,如果子线程 panic ,则返回给调用 panic! 的 Err 的值。
配置线程
新的线程可以通过 Builder 类型在产生新的线程之前进行配置,目前可以设置子线程的名称和堆栈大小。
use std::thread;
thread::Builder::new().name("child1".to_string()).spawn(move || {
println!("Hello, world!");
});
线程类型
线程是通过线程类型来表示的,你可以通过两种方式之一来获得。
- 通过生成一个新的线程,例如,使用 thread::spawn 函数,并在 JoinHandle上调用 thread。
- 通过使用 thread::current 函数请求当前线程。
Thread::current 函数即使对于不是通过这个模块的API产生的线程也可以使用。
thread-local存储
这个模块还为Rust程序提供了 thread-local 存储的实现。线程本地存储是一种将数据存储到全局变量中的方法,程序中的每个线程都有自己的副本。线程不共享这个数据,所以访问不需要同步。
线程-本地键拥有它所包含的值,当线程退出时,将销毁该值。它是用 thread_local! 宏创建的,可以包含任何 'static
的值(没有借用指针)。它提供了一个访问器函数 with,可以产生一个共享引用到指定的闭包的值。线程本地键只允许对值进行共享访问,因为如果容许可变借用,就没有办法保证唯一性。大多数值都希望通过Cell或RefCell类型利用某种形式的内部可变性。
命名线程
线程可以有相关的名称,以便于识别。默认情况下,生成的线程是不命名的。要指定一个线程的名称,用Builder构建线程,并将所需的线程名称传递给 Builder::name。要从线程中获取线程名称,使用Thread::name。几个例子说明了线程名称被使用的情况。
- 如果在一个已命名的线程中发生恐慌,线程名称将被打印在恐慌消息中。
- 线程的名字会被提供给操作系统(例如,在unix-like平台上的pthread_setname_np)。
栈的大小
产生线程的默认堆栈大小为 2 MiB,但这个特定的堆栈大小将来可能会改变。有两种方法可以手动指定生成线程的堆栈大小。
- 用Builder构建线程,并将所需的堆栈大小传递给 Builder::stack_size。
- 将 RUST_MIN_STACK 环境变量设置为代表所需堆栈大小的整数(单位为字节)。注意,设置Builder::stack_size将覆盖这个值。
注意,主线程的堆栈大小不是由Rust决定的。
6 - Rust标准库中的Boxed
6.1 - Rust标准库中的Boxed介绍
https://doc.rust-lang.org/std/boxed/index.html
用于堆分配的指针类型。
Box
例子
通过创建Box,将值从栈中移动到堆中:
let val: u8 = 5;
let boxed: Box<u8> = Box::new(val);
通过取消引用(dereferencing)将值从Box中移回栈中:
let boxed: Box<u8> = Box::new(5);
let val: u8 = *boxed;
创建一个递归数据结构:
#[derive(Debug)]
enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}
let list: List<i32> = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
println!("{:?}", list);
这将打印出 Cons(1, Cons(2, Nil))
。
递归结构必须是box,因为如果Cons的定义是这样的:
Cons(T, List<T>),
这样做就不行了。这是因为List的大小取决于列表中有多少元素,所以我们不知道要为Cons分配多少内存。通过引入一个Box
内存布局
TODO
7 - Rust标准库中的借用(borrow)
7.1 - Rust标准库中的借用介绍
https://doc.rust-lang.org/std/borrow/index.html
用于处理借用(borrow)数据的模块。
Cow 枚举
pub enum Cow<'a, B> where
B: 'a + ToOwned + ?Sized, {
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
clone-on-write 智能指针。
cow = Clone On Write
类型Cow是一个智能指针,提供了 clone-on-write 功能:它可以封装并提供对借用数据的不可变访问,当需要可变或所有权时,可以延迟克隆数据。该类型是通过 Borrow trait 来处理一般的借用数据。
Cow 实现了 Deref,这意味着你可以直接在它所封装的数据上调用非可变方法。如果需要改变,to_mut 将获得一个到拥有值的可变引用,必要时进行克隆。
use std::borrow::Cow;
fn abs_all(input: &mut Cow<[i32]>) {
for i in 0..input.len() {
let v = input[i];
if v < 0 {
// Clones into a vector if not already owned.
input.to_mut()[i] = -v;
}
}
}
// No clone occurs because `input` doesn't need to be mutated.
let slice = [0, 1, 2];
let mut input = Cow::from(&slice[..]);
abs_all(&mut input);
// Clone occurs because `input` needs to be mutated.
let slice = [-1, 0, 1];
let mut input = Cow::from(&slice[..]);
abs_all(&mut input);
// No clone occurs because `input` is already owned.
let mut input = Cow::from(vec![-1, 0, 1]);
abs_all(&mut input);
Borrow Trait
借用数据的特征。
在Rust中,常见的是为不同的用例提供不同的类型表示。例如,可以通过诸如Box
这些类型通过对该数据的类型的引用来提供对底层数据的访问。它们被说成是 “borrow/借用 “该类型。例如,Box
类型表示它们可以通过实现Borrow
此外,在为附加的特征提供实现时,需要考虑到它们是否应该与底层类型的行为完全相同,因为它们作为底层类型的表示方式。当依赖这些额外的特征实现的行为相同时,通用代码通常会使用Borrow
特别是Eq、Ord和Hash对于借用值和拥有值必须是等价的:x.borrow()==y.borrow()
应该给出与 x==y
相同的结果。
如果通用代码仅仅需要对所有能够提供相关类型T的引用的类型进行工作,那么通常最好使用AsRef
例子:
作为一个数据集合,HashMap<K, V>同时拥有键和值。如果键的实际数据被封装在某种管理类型中,但是,应该还是可以使用对键的数据引用来搜索值。例如,如果键是一个字符串,那么它很可能是用哈希图作为String存储的,而应该可以用&str来搜索。因此,insert需要对String进行操作,而get需要能够使用&str。
稍微简化一下,HashMap<K, V>的相关部分看起来像这样。
use std::borrow::Borrow;
use std::hash::Hash;
pub struct HashMap<K, V> {
// fields omitted
}
impl<K, V> HashMap<K, V> {
pub fn insert(&self, key: K, value: V) -> Option<V>
where K: Hash + Eq
{
// ...
}
pub fn get<Q>(&self, k: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized
{
// ...
}
}
整个hash map是通用于一个key类型K,由于这些key是和hash map一起存储的,所以这个类型必须拥有该密钥的数据。当插入一个键-值对时,map被赋予了这样一个K,需要找到正确的hash map,并根据这个K来检查这个键是否已经存在。
然而,当在地图中搜索一个值时,必须提供一个K的引用作为要搜索的键,这就需要始终创建这样一个拥有的值。对于字符串键,这就意味着需要创建一个String值来搜索只有str的情况。
相反,get方法是通用于底层键数据的类型,在上面的方法签名中称为Q。它通过要求K:Borrow来说明K借入为Q。通过额外要求Q:Hash + Eq,它表明要求K和Q都有Hash和Eq特征的实现,产生相同的结果。
get的实现特别依赖于Hash的相同实现,通过在Q值上调用Hash::hash来确定密钥的哈希桶,即使它是根据从K值计算出的哈希值插入了key。
因此,如果一个包裹Q值的K产生的散列值与Q不同,那么hash map就会被破坏。
pub struct CaseInsensitiveString(String);
impl PartialEq for CaseInsensitiveString {
fn eq(&self, : &Self) -> bool {
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl Eq for CaseInsensitiveString { }
因为两个相等的值需要产生相同的散列值,所以Hash的实现也需要忽略ASCII 大小写:
impl Hash for CaseInsensitiveString {
fn hash<H: Hasher>(&self, state: &mut H) {
for c in self.0.as_bytes() {
c.to_ascii_lowercase().hash(state)
}
}
}
CaseInsensitiveString可以实现Borrow
BorrowMut Trait
用于可变地借用数据的特征。
作为 Borrow
ToOwned Trait
用于借用数据的Clone泛化。
一些类型使其可以从借用数据到拥有数据,通常是通过实现Clone特征。但Clone只适用于从&T到T。ToOwned特征将Clone泛化为从给定类型的任何借入数据中构造出拥有的数据。
8 - Rust标准库中的cell
8.1 - Rust标准库中的cell介绍
https://doc.rust-lang.org/std/cell/index.html
可共享的可变容器。
Rust的内存安全就是基于这个规则。给定一个对象T,只可能有以下情况之一。
- 有该对象的多个不可变的引用(&T)(也称为别名/aliasing)。
- 有该对象的一个可变的引用(&mut T)(也称为可突变性/mutability)。
这是由Rust编译器强制执行的。但是,在有些情况下,这个规则不够灵活。有时需要对一个对象有多个引用,但又要对其进行改变。
可共享的可变容器的存在是为了允许以可控的方式进行修改,甚至在存在别名的情况下也是如此。Cell
Cell
Cell类型有两种风格:Cell
- 对于实现Copy的类型,get方法可以检索当前的内部值。
- 对于实现Default的类型,take方法用Default::default()替换当前内部值,并返回被替换的值。
- 对于所有类型,replace方法替换了当前的内部值并返回被替换的值,而 into_inner方法则消费 Cell
并返回内部值。此外,set方法替换了内部值,丢弃了被替换的值。
RefCell
何时选择内部可变性
比较常见的继承式可突变性,即必须有唯一的访问权限才能改变一个值,这是Rust语言的关键元素之一,它使Rust能够对指针别名进行有力的推理,从静态上防止了崩溃bug。正因为如此,继承式的可变性是首选,而内部可变性是不得已而为之。由于cell类型可以在不允许改变的地方进行改变,因此在某些情况下,内部突变可能是合适的,甚至是必须使用的,例如:
- 在不可变的事物 “内部"引入可变性
- 逻辑上不可变方法的实现细节。
- Clone的可变实现。
在不变的事物内部引入可变性
许多共享的智能指针类型,包括Rc
那么,在共享指针类型里面放一个RefCell
use std::cell::{RefCell, RefMut};
use std::collections::HashMap;
use std::rc::Rc;
fn main() {
let shared_map: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::new()));
// Create a new block to limit the scope of the dynamic borrow
{
let mut map: RefMut<_> = shared_map.borrow_mut();
map.insert("africa", 92388);
map.insert("kyoto", 11837);
map.insert("piccadilly", 11826);
map.insert("marbles", 38);
}
// Note that if we had not let the previous borrow of the cache fall out
// of scope then the subsequent borrow would cause a dynamic thread panic.
// This is the major hazard of using `RefCell`.
let total: i32 = shared_map.borrow().values().sum();
println!("{}", total);
}
注意,这个例子使用的是Rc
逻辑上不可更改的方法的实施细节
偶尔,在API中不公开 “在引擎盖下 “发生的修改可能是可取的。这可能是因为在逻辑上,操作是不可更改的,但例如,缓存会迫使实现者执行突变;也可能是因为你必须使用可变来实现一个trait方法,而这个trait方法最初定义为取&self。
use std::cell::RefCell;
struct Graph {
edges: Vec<(i32, i32)>,
span_tree_cache: RefCell<Option<Vec<(i32, i32)>>>
}
impl Graph {
fn minimum_spanning_tree(&self) -> Vec<(i32, i32)> {
self.span_tree_cache.borrow_mut()
.get_or_insert_with(|| self.calc_span_tree())
.clone()
}
fn calc_span_tree(&self) -> Vec<(i32, i32)> {
// Expensive computation goes here
vec![]
}
}
Clone的可变实现
这只是前者的一个特殊但是很常见的案例:隐藏看起来是不可改的操作中的可变性。克隆方法预计不会改变源值,并且声明取&self,而不是&mut self。因此,克隆方法中发生的任何改变都必须使用cell类型。例如,Rc
9 - Rust标准库中的克隆(clone)
9.1 - Rust标准库中的克隆介绍
https://doc.rust-lang.org/std/clone/index.html
克隆特性,用于不能 “隐式复制 “的类型。
在Rust中,一些简单的类型是 “隐式复制 “的,当你分配它们或传递它们作为参数时,接收方会得到一个副本,保留原值。这些类型不需要分配器来复制,也没有finalizers器(也就是说,它们不包含拥有的box或实现Drop),所以编译器认为它们的复制很便宜,也很安全。对于其他类型,必须显式地进行复制,通过实现Clone特征并调用clone方法。
基本用法示例:
let s = String::new(); // String type implements Clone
let copy = s.clone(); // so we can clone it
为了方便地实现Clone特征,也可以使用 #[derive(Clone)]
。例子:
#[derive(Clone)] // we add the Clone trait to Morpheus struct
struct Morpheus {
blue_pill: f32,
red_pill: i64,
}
fn main() {
let f = Morpheus { blue_pill: 0.0, red_pill: 0 };
let copy = f.clone(); // and now we can clone it!
}
10 - Rust标准库中的结果(result)
10.1 - Rust标准库中的结果(result)介绍
https://doc.rust-lang.org/std/result/index.html
使用Result类型的错误处理。
Result<T, E>是用于返回和传播错误的类型。它是一个枚举,其中Ok(T)代表成功并包含一值,Err(E)代表错误并包含错误值。
enum Result<T, E> {
Ok(T),
Err(E),
}
函数在预计到错误并可恢复时返回Result。在 std crate 中,Result 最显著的作用是用于 I/O。
一个简单的返回Result的函数可以这样定义和使用:
#[derive(Debug)]
enum Version { Version1, Version2 }
fn parse_version(header: &[u8]) -> Result<Version, &'static str> {
match header.get(0) {
None => Err("invalid header length"),
Some(&1) => Ok(Version::Version1),
Some(&2) => Ok(Version::Version2),
Some(_) => Err("invalid version"),
}
}
let version = parse_version(&[1, 2, 3, 4]);
match version {
Ok(v) => println!("working with version: {:?}", v),
Err(e) => println!("error parsing header: {:?}", e),
}
在Result上的模式匹配对于简单的案例来说是很清晰和直接的,但是Result自带的一些方便的方法可以让工作更简洁。
let good_result: Result<i32, i32> = Ok(10);
let bad_result: Result<i32, i32> = Err(10);
// The `is_ok` and `is_err` methods do what they say.
assert!(good_result.is_ok() && !good_result.is_err());
assert!(bad_result.is_err() && !bad_result.is_ok());
// `map` consumes the `Result` and produces another.
let good_result: Result<i32, i32> = good_result.map(|i| i + 1);
let bad_result: Result<i32, i32> = bad_result.map(|i| i - 1);
// Use `and_then` to continue the computation.
let good_result: Result<bool, i32> = good_result.and_then(|i| Ok(i == 11));
// Use `or_else` to handle the error.
let bad_result: Result<i32, i32> = bad_result.or_else(|i| Ok(i + 20));
// Consume the result and return the contents with `unwrap`.
let final_awesome_result = good_result.unwrap();
结果必须使用
使用返回值来表示错误,一个常见的问题是很容易忽略返回值,从而无法处理错误。Result被注释了 #[must_use] 属性,当忽略了Result值时,编译器会发出警告。这使得Result对于可能会遇到错误但不会返回有用值的函数特别有用。
考虑一下Write属性为I/O类型定义的write_all方法。
use std::io;
trait Write {
fn write_all(&mut self, bytes: &[u8]) -> Result<(), io::Error>;
}
注意:Write的实际定义使用的是io::Result,它只是Result<T, io::Error>的同义词。
这个方法不会产生一个值,但是写的时候可能会失败。关键是要处理好错误的情况,不要这样写:
use std::fs::File;
use std::io::prelude::*;
let mut file = File::create("valuable_data.txt").unwrap();
// If `write_all` errors, then we'll never know, because the return
// value is ignored.
file.write_all(b"important message");
如果你真的在Rust中写了,编译器会给你一个警告(默认情况下,由 unused_must_use lint 控制)。
相反,如果你不想处理这个错误,你可以直接用 expect 来断言成功。如果写入失败了,这将会panic,并提供一个略微有用的消息来说明原因。
use std::fs::File;
use std::io::prelude::*;
let mut file = File::create("valuable_data.txt").unwrap();
file.write_all(b"important message").expect("failed to write message");
也可以简单地断言成功:
assert!(file.write_all(b"important message").is_ok());
或者使用 ?
将错误传播到调用栈上:
fn write_message() -> io::Result<()> {
let mut file = File::create("valuable_data.txt")?;
file.write_all(b"important message")?;
Ok(())
}
问号运算符?
当编写调用许多返回结果类型的函数的代码时,错误处理可能会很繁琐。问号操作符 ?
隐藏了一些在调用堆栈中传播错误的繁文缛节。
下面这段代码:
use std::fs::File;
use std::io::prelude::*;
use std::io;
struct Info {
name: String,
age: i32,
rating: i32,
}
fn write_info(info: &Info) -> io::Result<()> {
// Early return on error
let mut file = match File::create("my_best_friends.txt") {
Err(e) => return Err(e),
Ok(f) => f,
};
if let Err(e) = file.write_all(format!("name: {}\n", info.name).as_bytes()) {
return Err(e)
}
if let Err(e) = file.write_all(format!("age: {}\n", info.age).as_bytes()) {
return Err(e)
}
if let Err(e) = file.write_all(format!("rating: {}\n", info.rating).as_bytes()) {
return Err(e)
}
Ok(())
}
将被替代为:
use std::fs::File;
use std::io::prelude::*;
use std::io;
struct Info {
name: String,
age: i32,
rating: i32,
}
fn write_info(info: &Info) -> io::Result<()> {
let mut file = File::create("my_best_friends.txt")?;
// Early return on error
file.write_all(format!("name: {}\n", info.name).as_bytes())?;
file.write_all(format!("age: {}\n", info.age).as_bytes())?;
file.write_all(format!("rating: {}\n", info.rating).as_bytes())?;
Ok(())
}
这样就好很多了!
以 ? 结束的表达式将导致成功(Ok)值的 unwrap,除非结果是Err,在这种情况下,Err会从包围函数中提前返回。
?只能用于返回 Result 的函数中,因为它提供了 Err 的提前返回。
Result Enum
#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
pub enum Result<T, E> {
Ok(T),
Err(E),
}
Result是一种类型,代表成功(Ok)或失败(Err)。
变量:
- Ok(T): 包含成功值
- Err(E):包含错误值
map_or 方法
对包含的值(如果有的话)应用一个函数,或者返回提供的默认值(如果没有的话)。
传递给map_or的参数会被立即求值;如果你传递的是函数调用的结果,建议使用map_or_else,它是延迟求值。
let x: Result<_, &str> = Ok("foo");
assert_eq!(x.map_or(42, |v| v.len()), 3);
let x: Result<&str, _> = Err("bar");
assert_eq!(x.map_or(42, |v| v.len()), 42);
map_or_else方法
通过将一个结果<T, E>映射到U,通过将一个函数应用到包含的Ok值,或者将一个fallback函数应用到包含的Err值。
这个函数可以用来在处理错误时解包一个成功的结果。
let k = 21;
let x : Result<_, &str> = Ok("foo");
assert_eq!(x.map_or_else(|e| k * 2, |v| v.len()), 3);
let x : Result<&str, _> = Err("bar");
assert_eq!(x.map_or_else(|e| k * 2, |v| v.len()), 42);
11 - Rust标准库中的标记(Marker)模块
原生 traits 和类型表示类型的基本属性。
Rust 类型可以根据其固有属性以各种有用的方式进行分类。 这些分类表示为 traits。
11.1 - Sized trait
11.1.1 - Sized trait的std文档
https://doc.rust-lang.org/std/marker/trait.Sized.html
pub trait Sized { }
在编译时已知常量大小的类型。
所有类型参数的隐含边界均为 Sized
。如果不合适,可以使用特殊语法 ?Sized
删除此绑定。
struct Foo<T>(T);
struct Bar<T: ?Sized>(T);
// struct FooUse(Foo<[i32]>); // 错误:没有为 [i32] 实现大小调整
struct BarUse(Bar<[i32]>); // OK
一个例外是 trait 的隐式 Self
类型。 trait 没有隐式 Sized
绑定,因为它与 trait 对象 不兼容,根据定义,trait 需要与所有可能的实现者一起使用,因此可以为任意大小。
尽管 Rust 允许您将 Sized
绑定到 trait,但是以后您将无法使用它来形成 trait 对象:
trait Foo { }
trait Bar: Sized { }
struct Impl;
impl Foo for Impl { }
impl Bar for Impl { }
let x: &dyn Foo = &Impl; // OK
// let y: &dyn Bar = &Impl; // 错误:无法将 trait `Bar` 创建成对象
备注:trait object的要求之一就是trait不能是Sized,这也可以作为禁止将某个trait用作trait object的方式
11.1.2 - Sized trait的源码
https://github.com/rust-lang/rust/blob/master/library/core/src/marker.rs
#[stable(feature = "rust1", since = "1.0.0")]
#[lang = "sized"]
#[rustc_on_unimplemented(
message = "the size for values of type `{Self}` cannot be known at compilation time",
label = "doesn't have a size known at compile-time"
)]
#[fundamental] // for Default, for example, which requires that `[T]: !Default` be evaluatable
#[rustc_specialization_trait]
pub trait Sized {
// Empty.
}
11.1.3 - [Rust编程之道笔记]Sided trait
Sized trait 非常重要,编译期用它来识别在编译期确定大小的类型。
#[lang = "sized"]
pub trait Sized {
// Empty.
}
Sized trait 是空 trait,仅仅作为标签 trait 供编译期使用。真正起"打标签"作用的是属性 #[lang = "sized"]
。该属性lang表示Sized trait供rust语言本身使用,声明为 “sized”,称为语言项(Lang Item)。
rust语言中大部分类型都是默认 Sized ,如果需要使用动态大小类型,则需要改为 <T: ?Sized>
限定。
struct Foo<T>(T);
struct Bar<T: ?Sized>(T);
Sized, Unsize和 ?Sized的关系
-
Sized 标记的是在编译期可确定大小的类型
-
Unsized 标记的是动态大小类型,在编译期无法确定其大小
目前rust中的动态类型有 trait 和 [T]
其中 [T] 代表一定数量的T在内存中的一次排列,但不知道具体的数量,所以大小是未知的。
-
?Sized 标记的类型包含了 Sized 和 Unsized 所标识的两种类型。
所以泛型结构体
struct Bar<T: ?Sized>(T);
支持编译期可确定大小类型和动态大小类型两种类型。
动态大小类型的限制规则
- 只可以通过胖指针来操作 Unsize 类型,如
&[T]
或者&trait
- 变量,参数和枚举变量不能使用动态大小类型
- 结构体中只有最有一个字段可以使用动态大小类型,其他字段不可以使用
11.2 - Unsized trait
11.2.1 - Unsize trait的std文档
https://doc.rust-lang.org/std/marker/trait.Unsize.html
pub trait Unsize<T: ?Sized> {}
可以是未定义大小的类型也可以是动态大小的类型。
例如,按大小排列的数组类型 [i8; 2]
实现 Unsize<[i8]>
和 Unsize<dyn fmt::Debug>
。
Unsize
的所有实现均由编译器自动提供。
Unsize
为以下目的实现:
-
[T; N]
是Unsize<[T]>
-
当
T: Trait
时T
为Unsize<dyn Trait>
-
Foo<..., T, ...>
是Unsize<Foo<..., U, ...>>
,如果T: Unsize<U>
- Foo 是一个结构体
- 仅
Foo
的最后一个字段具有涉及T
的类型 T
不属于任何其他字段的类型Bar<T>: Unsize<Bar<U>>
, 如果Foo
的最后一个字段的类型为Bar<T>
Unsize
与 ops::CoerceUnsized
一起使用可允许 “user-defined” 容器 (例如 Rc
包含动态大小的类型。 有关更多详细信息,请参见 DST coercion RFC 和 the nomicon entry on coercion。
11.2.2 - Unsize trait的源码
https://github.com/rust-lang/rust/blob/master/library/core/src/marker.rs
#[unstable(feature = "unsize", issue = "27732")]
#[lang = "unsize"]
pub trait Unsize<T: ?Sized> {
// Empty.
}
11.3 - Copy trait
11.3.1 - Copy trait的std文档
https://doc.rust-lang.org/std/marker/trait.Copy.html
pub trait Copy: Clone { }
只需复制位即可复制其值的类型。
默认情况下,变量绑定具有 move语义
。换句话说:
#[derive(Debug)]
struct Foo;
let x = Foo;
let y = x;
// `x` 已移至 `y`,因此无法使用
// println!("{:?}", x); // error: use of moved value
但是,如果类型实现 Copy
,则它具有复制语义:
// 我们可以派生一个 `Copy` 实现。
// `Clone` 也是必需的,因为它是 `Copy` 的父特征。
#[derive(Debug, Copy, Clone)]
struct Foo;
let x = Foo;
let y = x;
// `y` 是 `x` 的副本
println!("{:?}", x); // A-OK!
重要的是要注意,在这两个示例中,唯一的区别是分配后是否允许您访问 x
。 在后台,复制(copy)和移动(move)都可能导致将位复制到内存中,尽管有时会对其进行优化。
如何实现 Copy
?
有两种方法可以在您的类型上实现 Copy
。最简单的是使用 derive
:
#[derive(Copy, Clone)]
struct MyStruct;
您还可以手动实现 Copy
和 Clone
:
struct MyStruct;
impl Copy for MyStruct { }
impl Clone for MyStruct {
fn clone(&self) -> MyStruct {
*self
}
}
两者之间的区别很小: derive
策略还将 Copy
绑定在类型参数上,这并不总是需要的。
Copy
和 Clone
有什么区别?
复制是隐式发生的,例如作为分配 y = x
的一部分。Copy
的行为不可重载; 它始终是简单的按位复制。
克隆是一个明确的动作 x.clone()
。Clone
的实现可以提供安全复制值所需的任何特定于类型的行为。 例如,用于 String
的 Clone
的实现需要在堆中复制指向字符串的缓冲区。 String
值的简单按位副本将仅复制指针,从而导致该行向下双重释放。 因此,String
是 Clone
,但不是 Copy
。
Clone
是 Copy
的父特征,因此 Copy
的所有类型也必须实现 Clone
。 如果类型为 Copy
,则其 Clone
实现仅需要返回 *self
(请参见上面的示例)。
类型何时可以是 Copy
?
如果类型的所有组件都实现 Copy
,则它可以实现 Copy
。例如,此结构体可以是 Copy
:
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
一个结构体可以是 Copy
,而 i32
是 Copy
,因此 Point
有资格成为 Copy
。 相比之下,考虑
struct PointList {
points: Vec<Point>,
}
结构体 PointList
无法实现 Copy
,因为 Vec
不是 Copy
。如果尝试派生 Copy
实现,则会收到错误消息:
the trait `Copy` may not be implemented for this type; field `points` does not implement `Copy`
共享引用 (&T
) 也是 Copy
,因此,即使类型中包含不是 *Copy
类型的共享引用 T
,也可以是 Copy
。 考虑下面的结构体,它可以实现 Copy
,因为它从上方仅对我们的非 Copy 类型 PointList
持有一个 shared 引用:
#[derive(Copy, Clone)]
struct PointListWrapper<'a> {
point_list_ref: &'a PointList,
}
什么时候类型不能为 Copy
?
某些类型无法安全复制。例如,复制 &mut T
将创建一个别名可变引用。 复制 String
将重复管理 String
缓冲区,从而导致双重释放。
概括后一种情况,任何实现 Drop
的类型都不能是 Copy
,因为它除了管理自己的 size_of::
字节外还管理一些资源。
果您尝试在包含非 Copy
数据的结构或枚举上实现 Copy
,则会收到 E0204 错误。
什么时候类型应该是 Copy
?
一般来说,如果您的类型可以实现 Copy
,则应该这样做。 但是请记住,实现 Copy
是您类型的公共 API 的一部分。 如果该类型将来可能变为非 Copy
,则最好现在省略 Copy
实现,以避免 API 发生重大更改。
其他实现者
除下面列出的实现者外,以下类型还实现 Copy
:
- 函数项类型 (即,为每个函数定义的不同类型)
- 函数指针类型 (例如
fn() -> i32
) - 如果项类型也实现
Copy
(例如[i32; 123456]
),则所有大小的数组类型 - 如果每个组件还实现
Copy
(例如()
,(i32, bool)
),则为元组类型 - 闭包类型,如果它们没有从环境中捕获任何值,或者所有此类捕获的值本身都实现了
Copy
。 请注意,由共享引用捕获的变量始终实现Copy
(即使引用对象没有实现),而由变量引用捕获的变量从不实现Copy
。
11.3.2 - Copy trait的源码
https://github.com/rust-lang/rust/blob/master/library/core/src/marker.rs
#[stable(feature = "rust1", since = "1.0.0")]
#[lang = "copy"]
#[rustc_unsafe_specialization_marker]
pub trait Copy: Clone {
// Empty.
}
原始类型的Copy的实现。
无法在Rust中描述的实现是在 rustc_trait_selection 中的 traits::SelectionContext::copy_clone_conditions()
中实现。
mod copy_impls {
use super::Copy;
macro_rules! impl_copy {
($($t:ty)*) => {
$(
#[stable(feature = "rust1", since = "1.0.0")]
impl Copy for $t {}
)*
}
}
impl_copy! {
usize u8 u16 u32 u64 u128
isize i8 i16 i32 i64 i128
f32 f64
bool char
}
#[unstable(feature = "never_type", issue = "35121")]
impl Copy for ! {}
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Copy for *const T {}
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Copy for *mut T {}
/// Shared references can be copied, but mutable references *cannot*!
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Copy for &T {}
}
11.3.3 - [Rust编程之道笔记]Copy trait
copy trait 用来标识可以按位复制其值的类型,按位复制等价于c语言中的 memcpy。
pub trait Copy: Clone { }
copy trait 继承自 clone trait,意味着要实现 Copy trait 的类型,必须实现 Clone trait 中定义的方法。
要实现 Copy trait,就必须同时实现 Clone trait。
struct MyStruct;
impl Copy for MyStruct { }
impl Clone for MyStruct {
fn clone(&self) -> MyStruct {
*self
}
}
rust提供了更方便的 derive 属性:
#[derive(Copy, Clone)]
struct MyStruct;
copy 的行为是隐式行为,开发者不能重载 Copy 行为,它永远都是一个简单的位复制。
并非所有的类型都可以实现 Copy trait。
11.4 - Send trait
11.4.1 - Send trait的源码
https://github.com/rust-lang/rust/blob/master/library/core/src/marker.rs
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), rustc_diagnostic_item = "send_trait")]
#[rustc_on_unimplemented(
message = "`{Self}` cannot be sent between threads safely",
label = "`{Self}` cannot be sent between threads safely"
)]
pub unsafe auto trait Send {
// empty.
}
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> !Send for *const T {}
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> !Send for *mut T {}
11.4.2 - Send trait的std文档
https://doc.rust-lang.org/std/marker/trait.Send.html
pub unsafe auto trait Send { }
可以跨线程边界传输的类型。
当编译器确定适当时,会自动实现此 trait。
非 Send
类型的一个示例是引用计数指针 rc::Rc
。 如果两个线程试图克隆指向相同引用计数值的 Rc
,它们可能会同时尝试更新引用计数,这是 未定义行为 因为 Rc
不使用原子操作。
它的表亲 sync::Arc
确实使用原子操作 (产生一些开销),因此它是 Send
。
有关更多详细信息,请参见 the Nomicon。
11.4.3 - [Rust编程之道笔记]Send trait
rust 提供了 Send 和 Sync 两个标签 trait,他们是 rust 无数据竞争并发的基石。
- 实现了 Send 的类型,可以安全的在线程间传递值,也就是说可以跨线程传递所有权
- 实现了 Sync 的类型,可以跨线程安全地传递共享(不可变)引用
有了这两个标签,就可以把rust中所有的类型归为两类:
- 可以安全跨线程传递的值和引用
- 不可以安全跨线程传递的值和引用
在配合所有权机制,带来的效果就是,rust 能够在编译期就检查出数据竞争的隐患,而不需要到运行时再排查。
# 备注:这行代码在 marker.rs 中已经找不到了,不知道
# 为所有类型实现 Send
unsafe impl Send for .. {}
#[stable(feature = "rust1", since = "1.0.0")]
# 使用 !Send 语法排除 *const T 和 *mut T
impl<T: ?Sized> !Send for *const T {}
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> !Send for *mut T {}
11.4.4 - send和sync
https://doc.rust-lang.org/nomicon/send-and-sync.html
不过,并不是所有的东西都服从于继承的可变性。有些类型允许你在内存中对一个位置有多个别名,同时对其进行突变。除非这些类型使用同步化来管理这种访问,否则它们绝对不是线程安全的。Rust通过Send和Sync特性捕捉到这一点。
- 如果将一个类型发送到另一个线程是安全的,那么它就是Send。
- 如果一个类型可以安全地在线程间共享,那么它就是 Sync(当且仅当
&T
是 Send 时,T 是 Sync)。
Send 和 Sync 是Rust的并发故事的基础。因此,存在大量的特殊工具来使它们正常工作。首先,它们是不安全的特性。这意味着它们的实现是不安全的,而其他不安全的代码可以认为它们是正确实现的。由于它们是标记性的特征(它们没有像方法那样的关联项),正确实现仅仅意味着它们具有实现者应该具有的内在属性。不正确地实现 Send 或 Sync 会导致未定义行为。
Send 和 Sync 也是自动派生的特性。这意味着,与其它特质不同,如果一个类型完全由 Send 或 Sync 类型组成,那么它就是 Send 或 Sync。几乎所有的基元都是 Send 和 Sync,因此,几乎所有你将与之交互的类型都是 Send 和 Sync。
主要的例外情况包括:
- 原始指针既不是 Send 也不是 Sync(因为它们没有安全防护)。
- UnsafeCell 不是Sync(因此Cell和RefCell也不是)。
- Rc不是Send或Sync(因为refcount是共享的,而且是不同步的)。
Rc 和 UnsafeCell 从根本上说不是线程安全的:它们启用了非同步的共享可变体状态。然而,严格来说,原始指针被标记为线程不安全,更像是一种提示。对原始指针做任何有用的事情都需要对其进行解引用,这已经是不安全的了。从这个意义上说,人们可以争辩说,将它们标记为线程安全是 “好的”。
然而,重要的是,它们不是线程安全的,以防止包含它们的类型被自动标记为线程安全的。这些类型有非实质性的未跟踪的所有权,它们的作者不可能认真考虑线程安全问题。在Rc的例子中,我们有一个包含 *mut
的类型的好例子,它绝对不是线程安全的。
如果需要的话,那些没有自动派生的类型可以简单地实现它们。
struct MyBox(*mut u8);
unsafe impl Send for MyBox {}
unsafe impl Sync for MyBox {}
在极其罕见的情况下,一个类型被不适当地自动派生为Send或Sync,那么我们也可以不实现Send和Sync。
#![feature(negative_impls)]
// I have some magic semantics for some synchronization primitive!
struct SpecialThreadToken(u8);
impl !Send for SpecialThreadToken {}
impl !Sync for SpecialThreadToken {}
注意,就其本身而言,不可能错误地派生出 Send 和 Sync。只有那些被其他不安全代码赋予特殊含义的类型才有可能通过错误的 Send 或 Sync 引起麻烦。
大多数对原始指针的使用应该被封装在一个足够的抽象后面,以便 Send 和 Sync 可以被派生。例如,所有Rust的标准集合都是 Send 和 Sync(当它们包含Send和Sync类型时),尽管它们普遍使用原始指针来管理分配和复杂的所有权。同样地,大多数进入这些集合的迭代器都是Send和Sync的,因为它们在很大程度上表现为进入集合的 &
或 &mut
。
例子
由于各种原因,Box被编译器实现为它自己的特殊内在类型,但是我们可以自己实现一些具有类似行为的东西,看看什么时候实现 Send 和 Sync 是合理的。让我们把它叫做 “Carton”。
我们先写代码,把一个分配在栈上的值,转移到堆上。
#![allow(unused)]
fn main() {
pub mod libc {
pub use ::std::os::raw::{c_int, c_void};
#[allow(non_camel_case_types)]
pub type size_t = usize;
extern "C" { pub fn posix_memalign(memptr: *mut *mut c_void, align: size_t, size: size_t) -> c_int; }
}
use std::{
mem::{align_of, size_of},
ptr,
};
struct Carton<T>(ptr::NonNull<T>);
impl<T> Carton<T> {
pub fn new(value: T) -> Self {
// Allocate enough memory on the heap to store one T.
assert_ne!(size_of::<T>(), 0, "Zero-sized types are out of the scope of this example");
let mut memptr = ptr::null_mut() as *mut T;
unsafe {
let ret = libc::posix_memalign(
(&mut memptr).cast(),
align_of::<T>(),
size_of::<T>()
);
assert_eq!(ret, 0, "Failed to allocate or invalid alignment");
};
// NonNull is just a wrapper that enforces that the pointer isn't null.
let mut ptr = unsafe {
// Safety: memptr is dereferenceable because we created it from a
// reference and have exclusive access.
ptr::NonNull::new(memptr.cast::<T>())
.expect("Guaranteed non-null if posix_memalign returns 0")
};
// Move value from the stack to the location we allocated on the heap.
unsafe {
// Safety: If non-null, posix_memalign gives us a ptr that is valid
// for writes and properly aligned.
ptr.as_ptr().write(value);
}
Self(ptr)
}
}
}
这不是很有用,因为一旦我们的用户给了我们一个值,他们就没有办法访问它。Box实现了 Deref
和 DerefMut
,这样你就可以访问内部的值。让我们来做这件事。
#![allow(unused)]
fn main() {
use std::ops::{Deref, DerefMut};
struct Carton<T>(std::ptr::NonNull<T>);
impl<T> Deref for Carton<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe {
// Safety: The pointer is aligned, initialized, and dereferenceable
// by the logic in [`Self::new`]. We require writers to borrow the
// Carton, and the lifetime of the return value is elided to the
// lifetime of the input. This means the borrow checker will
// enforce that no one can mutate the contents of the Carton until
// the reference returned is dropped.
self.0.as_ref()
}
}
}
impl<T> DerefMut for Carton<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe {
// Safety: The pointer is aligned, initialized, and dereferenceable
// by the logic in [`Self::new`]. We require writers to mutably
// borrow the Carton, and the lifetime of the return value is
// elided to the lifetime of the input. This means the borrow
// checker will enforce that no one else can access the contents
// of the Carton until the mutable reference returned is dropped.
self.0.as_mut()
}
}
}
}
最后,让我们考虑一下我们的Carton是否是Send和Sync。有些东西可以安全地成为 “Send”,除非它与其他东西共享可变的状态而不对其进行排他性访问。每个Carton都有一个唯一的指针,所以我们很好。
// Safety: No one besides us has the raw pointer, so we can safely transfer the
// Carton to another thread if T can be safely transferred.
unsafe impl<T> Send for Carton<T> where T: Send {}
Sync如何?为了使Carton同步,我们必须强制规定,你不能写到存储在一个 &Carton
中的东西,而这个东西可以从另一个 &Carton中
读到或写入。由于你需要一个 &mut Carton
来写入指针,并且 borrow 检查器强制要求可变引用必须是独占的,所以Carton也不存在健全性问题,可以同步。
// Safety: Since there exists a public way to go from a `&Carton<T>` to a `&T`
// in an unsynchronized fashion (such as `Deref`), then `Carton<T>` can't be
// `Sync` if `T` isn't.
// Conversely, `Carton` itself does not use any interior mutability whatsoever:
// all the mutations are performed through an exclusive reference (`&mut`). This
// means it suffices that `T` be `Sync` for `Carton<T>` to be `Sync`:
unsafe impl<T> Sync for Carton<T> where T: Sync {}
当我们断言我们的类型是Send and Sync时,我们通常需要强制要求每个包含的类型都是Send and Sync。当编写行为像标准库类型的自定义类型时,我们可以断言我们有同样的要求。例如,下面的代码断言,如果同类型的Box是Send,那么Carton就是Send,在这种情况下,这就等于说T是Send。
unsafe impl<T> Send for Carton<T> where Box<T>: Send {}
现在,Carton
impl<T> Drop for Carton<T> {
fn drop(&mut self) {
unsafe {
libc::free(self.0.as_ptr().cast());
}
}
}
一个不发生这种情况的好例子是MutexGuard:注意它不是Send。MutexGuard的实现使用了一些库,这些库要求你确保你不会试图释放你在不同线程中获得的锁。如果你能够将MutexGuard发送到另一个线程,那么析构器就会在你发送它的线程中运行,这就违反了要求。MutexGuard仍然可以被同步,因为你能发送给另一个线程的只是一个&MutexGuard,而丢弃一个引用什么也做不了。
TODO:更好地解释什么可以或不可以是Send或Sync。仅仅针对数据竞赛就足够了吗?
备注: 没看懂。。。
11.5 - Sync trait
11.5.1 - Sync trait的std文档
https://doc.rust-lang.org/std/marker/trait.Sync.html
pub unsafe auto trait Sync { }
可以在线程之间安全共享引用的类型。
当编译器确定适当时,会自动实现此 trait。
精确的定义是:当且仅当 &T
是 Send
时,类型 T
才是 Sync
。 换句话说,如果在线程之间传递 &T
引用时没有 未定义的行为 (包括数据竞争) 的可能性。
正如人们所料,像 u8
和 f64
这样的原始类型都是 Sync
,包含它们的简单聚合类型也是如此,比如元组、结构体和枚举。 基本 Sync
类型的更多示例包括不可变类型 (例如 &T
) 以及具有简单继承的可变性的类型,例如 Box
,Vec
和大多数其他集合类型。
该定义的一个令人惊讶的结果是 &mut T
是 Sync
(如果 T
是 Sync
),即使看起来可能提供了不同步的可变的。 诀窍是,共享引用 (即 & &mut T
) 后面的可变引用将变为只读,就好像它是 & &T
一样。 因此,没有数据竞争的风险。
不是 Sync
的类型是具有非线程安全形式的 “内部可变性” 的类型,例如 Cell
和 RefCell
。 这些类型甚至允许通过不可变,共享引用来更改其内容。 例如,Cell
上的 set
方法采用 &self
,因此它仅需要共享的引用 &Cell
。 该方法不执行同步,因此 Cell
不能为 Sync
。
另一个非 Sync
类型的例子是引用计数指针 Rc
。 给定任何引用 &Rc
,您可以克隆新的 Rc
,以非原子方式修改引用计数。
对于确实需要线程安全的内部可变性的情况,Rust 提供 原子数据类型 以及通过 sync::Mutex
和 sync::RwLock
进行的显式锁定。 这些类型可确保任何可变的都不会引起数据竞争,因此类型为 Sync
。 同样,sync::Arc
提供了 Rc
的线程安全模拟。
任何具有内部可变性的类型还必须在 value(s) 周围使用 cell::UnsafeCell
包装器,该包装器可以通过共享的引用进行更改。 未定义的行为 无法做到这一点。 例如,从 &T
到 &mut T
的 transmute
无效。
有关 Sync
的更多详细信息,请参见 the Nomicon。
11.5.2 - Sync trait的源码
https://github.com/rust-lang/rust/blob/master/library/core/src/marker.rs
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), rustc_diagnostic_item = "sync_trait")]
#[lang = "sync"]
#[rustc_on_unimplemented(
message = "`{Self}` cannot be shared between threads safely",
label = "`{Self}` cannot be shared between threads safely"
)]
pub unsafe auto trait Sync {
// Empty
}
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> !Sync for *const T {}
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> !Sync for *mut T {}
11.6 - Unpin trait
11.6.1 - Unsize trait的std文档
https://doc.rust-lang.org/std/marker/trait.Unpin.html
pub auto trait Unpin { }
固定后可以安全移动的类型。
Rust 本身没有固定类型的概念,并认为 move (例如,通过赋值或 mem::replace
始终是安全的。
Pin
类型代替使用,以防止在类型系统中移动。Pin<P<T>>
包装器中包裹的指针 P<T>
不能移出。 有关固定的更多信息,请参见 pin
module 文档。
为 T
实现 Unpin
trait 消除了固定该类型的限制,然后允许使用诸如 mem::replace
之类的功能将 T
从 Pin<P<T>>
中移出。
Unpin
对于非固定数据完全没有影响。 特别是,mem::replace
可以愉快地移动 !Unpin
数据 (它适用于任何 &mut T
,而不仅限于 T: Unpin
)。 但是,您不能对包装在 Pin<P<T>>
内的数据使用 mem::replace
,因为您无法获得所需的 &mut T
,并且 that 是使此系统正常工作的原因。
因此,例如,这只能在实现 Unpin
的类型上完成:
use std::mem;
use std::pin::Pin;
let mut string = "this".to_string();
let mut pinned_string = Pin::new(&mut string);
// 我们需要一个可变引用来调用 `mem::replace`。
// 我们可以通过 (implicitly) 调用 `Pin::deref_mut` 来获得这样的引用,但这仅是可能的,因为 `String` 实现了 `Unpin`。
mem::replace(&mut *pinned_string, "other".to_string());
trait 几乎针对每种类型自动实现。
11.6.2 - Unpin trait的源码
https://github.com/rust-lang/rust/blob/master/library/core/src/marker.rs
#[stable(feature = "pin", since = "1.33.0")]
#[rustc_on_unimplemented(
note = "consider using `Box::pin`",
message = "`{Self}` cannot be unpinned"
)]
#[lang = "unpin"]
pub auto trait Unpin {}
/// A marker type which does not implement `Unpin`.
///
/// If a type contains a `PhantomPinned`, it will not implement `Unpin` by default.
#[stable(feature = "pin", since = "1.33.0")]
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct PhantomPinned;
#[stable(feature = "pin", since = "1.33.0")]
impl !Unpin for PhantomPinned {}
#[stable(feature = "pin", since = "1.33.0")]
impl<'a, T: ?Sized + 'a> Unpin for &'a T {}
#[stable(feature = "pin", since = "1.33.0")]
impl<'a, T: ?Sized + 'a> Unpin for &'a mut T {}
#[stable(feature = "pin_raw", since = "1.38.0")]
impl<T: ?Sized> Unpin for *const T {}
#[stable(feature = "pin_raw", since = "1.38.0")]
impl<T: ?Sized> Unpin for *mut T {}