rust高级进阶总结
文章目录
- 前言
- 1. Rust生命周期进阶
- 一、不太聪明的生命周期检查
- (一)例子1
- (二)例子2
- 二、无界生命周期
- 三、生命周期约束(HRTB)
- (一)语法及含义
- (二)综合例子
- 四、闭包函数的消除规则
- (一)问题示例
- (二)原因分析
- (三)解决方法
- 五、NLL(Non - Lexical Lifetime)
- (一)规则变化
- (二)示例及分析
- 六、Reborrow(再借用)
- (一)概念及示例
- (二)错误示例
- 七、生命周期消除规则补充
- (一)impl块消除
- (二)生命周期约束消除
- 八、复杂例子分析
- (一)代码及错误
- (二)原因分析
- (三)解决方法
- 2. `&'static`和`T: 'static`
- 一、`'static`的常见情况
- (一)字符串字面量
- (二)特征对象
- 二、`&'static`
- (一)含义
- (二)示例及注意事项
- 三、`T: 'static`
- (一)约束情况
- (二)示例及分析
- 1. 函数中直接使用`T`
- 2. 函数中使用`&T`
- 3. 另一个示例
- 四、`&'static`和`T: 'static`的区别
- (一)针对对象
- 3.闭包笔记
- 一、闭包的定义和基本使用
- (一)定义
- (二)语法形式
- (三)类型推导
- 二、闭包用于简化代码
- (一)传统函数实现问题
- (二)函数变量实现的不足
- (三)闭包实现的优势
- 三、结构体中的闭包
- (一)结构体设计
- (二)方法实现
- (三)局限性及改进方向
- 四、闭包捕获作用域中的值
- (一)闭包的特性
- (二)函数的限制
- (三)闭包对内存的影响
- 六、三种`Fn`特征
- (一)`FnOnce`特征
- (二)`FnMut`特征
- (三)`Fn`特征
- (四)三种特征的关系
- 七、闭包作为函数返回值
- (一)返回闭包的问题
- (二)解决方法
- (三)最终解决方案
- 4.迭代器Interator
- 一、迭代器基础
- (一)迭代器与`for`循环
- (二)`IntoIterator`特征
- (三)迭代器的惰性初始化
- (四)`next`方法
- 二、迭代器相关方法和类型转换
- (一)`into_iter`、`iter`和`ter_mut`
- (二)`Iterator`和`IntoIterator`的区别
- 三、消费者与适配器
- (一)消费者
- (二)迭代器适配器
- (三)闭包作为适配器参数
- 四、实现`Iterator`特征
- (一)自定义迭代器示例
- (二)`Iterator`特征的其它方法
- 五、`enumerate`方法
- 六、迭代器的性能
- 5.类型转换
- 一、`as`转换
- (一)基本类型转换
- (二)内存地址转换为指针
- 二、`TryInto`转换
- (一)使用场景
- (二)错误处理
- 三、通用类型转换
- (一)结构体转换示例
- 四、强制类型转换
- (一)特征匹配
- (二)点操作符
- 五、变形记(`Transmutes`)
- (一)`mem::transmute`
- (二)`mem::transmute_copy`
- 六、`newtype`
- (一)定义和用途
- (二)示例
- 七、类型别名(`Type Alias`)
- (一)定义和特点
- (二)应用场景
- 八、`!`永不返回类型
- 九、`Sized`和不定长类型`DST`
- (一)不定长类型`DST`
- (二)`Sized`特征
- 十、整数转换为枚举
- (一)C语言实现
- (二)Rust实现方式
- 6.`Box<T>`堆对象分配
- 一、Rust中的堆栈
- (一)堆栈概念
- (二)堆栈性能
- 二、`Box<T>`的使用场景
- (一)将数据存储在堆上
- (二)避免栈上数据拷贝
- (三)将动态大小类型变为Sized固定大小类型
- (四)特征对象
- 三、`Box`内存布局
- (一)`Vec<i32>`内存布局
- (二)`Vec<Box<i32>>`内存布局
- 四、`Box::leak`
- (一)功能
- (二)示例
- 7.`Deref`解引用
- 一、`Deref`的引入
- (一)问题示例
- (二)智能指针与`Deref`
- 二、常规引用解引用
- 三、智能指针解引用
- (一)自定义智能指针
- (二)为自定义智能指针实现`Deref`特征
- (三)`*`背后的原理
- 四、函数和方法中的隐式`Deref`转换
- (一)基本隐式转换
- (二)连续隐式转换
- (三)在方法、赋值中的应用
- 五、`Deref`规则总结
- (一)基本规则
- (二)引用归一化
- 六、三种`Deref`转换
- (一)不可变`Deref`转换
- (二)可变`Deref`转换
- (三)可变转不可变`Deref`转换
- (四)示例
- 七、总结
- 8.Drop`释放资源
- 一、`Drop`特征的作用
- 二、`Drop`示例
- (一)结构体的`Drop`实现
- (二)未实现`Drop`的结构体
- 三、`Drop`顺序
- 四、手动回收
- (一)手动`drop`的问题
- (二)正确的手动`drop`方式
- 五、`Drop`使用场景
- 六、`Copy`和`Drop`的互斥
- 9.智能指针Rc、Arc、Cell、RefCell
- 一、Rc与Arc
- (一)引入原因
- (二)Rc<T>
- (三)Arc
- 二、Cell与RefCell
- (一)引入原因
- (二)Cell<T>
- (三)RefCell<T>
- (四)内部可变性
- (五)Rc + RefCell组合使用
- (六)通过Cell::from_mut解决借用冲突
- 10.循环引用与结构体自引用
- 一、循环引用
- (一)循环引用的产生
- (二)Weak解决循环引用
- (三)unsafe解决循环引用
- 二、结构体自引用
- (一)自引用结构体的问题
- (二)unsafe实现自引用
- (三)Pin实现自引用
- (四)ouroboros库实现自引用
- (五)其他相关库
- (六)Rc + RefCell或Arc + Mutex解决自引用
- (七)终极大法
- (八)学习资源推荐
- 11.并发与线程
- 一、并发和并行
- (一)概念区别
- (二)编程语言的并发模型
- 二、使用线程
- (一)多线程编程的风险
- (二)创建线程
- (三)等待子线程的结束
- (四)在线程闭包中使用`move`
- (五)线程是如何结束的
- (六)多线程的性能
- (七)线程屏障(Barrier)
- (八)线程局部变量(Thread Local Variable)
- (九)用条件控制线程的挂起和执行
- (十)只被调用一次的函数
- 12. 线程间消息传递
- 一、消息通道
- (一)多发送者,单接收者(`mpsc`)
- 1. 创建与使用
- 2. 类型推导与所有权
- 3. 循环接收与多发送者
- 4. 消息顺序
- (二)同步和异步通道
- 1. 异步通道
- 2. 同步通道
- (三)关闭通道
- (四)传输多种类型的数据
- 二、新手容易遇到的坑
- (一)示例问题
- (二)解决办法
- 三、`mpmc`更好的性能
- (一)第三方库介绍
- 1. `crossbeam-channel`
- 2. `flume`
- 13.线程同步
- 一、线程同步方式选择
- (一)共享内存与消息传递
- 二、互斥锁`Mutex`
- (一)单线程中使用`Mutex`
- (二)多线程中使用`Mutex`
- (三)使用`Mutex`的注意事项
- 三、读写锁`RwLock`
- (一)使用规则
- 四、条件变量`Condvar`
- 五、信号量`Semaphore`
- 六、三方库提供的锁实现
- 14.Atomic原子操作与内存顺序
- 一、Atomic原子类型介绍
- 二、Atomic作为全局变量使用
- 三、内存顺序
- (一)影响因素
- (二)规则枚举
- (三)内存屏障例子
- (四)内存顺序选择
- 四、多线程中使用Atomic
- 五、Atomic与锁的比较
- (一)能否替代锁
- (二)Atomic应用场景
- 15.基于Send和Sync的线程安全
- 一、无法用于多线程的`Rc`
- (一)示例代码及报错
- (二)`Rc`和`Arc`源码对比
- 二、Send和Sync特征
- (一)特征作用
- (二)`RwLock`和`Mutex`的实现对比
- 三、实现Send和Sync的类型
- (一)默认实现情况
- (二)常见未实现的类型
- (三)自定义复合类型
- 四、为裸指针实现Send和Sync
- (一)为裸指针实现Send
- (二)为裸指针实现Sync
- 五、总结
- 16.全局变量
- 一、全局变量概述
- 二、编译期初始化
- (一)静态常量
- (二)静态变量
- (三)原子类型
- (四)示例:全局ID生成器
- 三、运行期初始化
- (一)问题引入
- (二)lazy_static
- (三)Box::leak
- (四)从函数中返回全局变量
- (五)标准库中的OnceCell
- 四、总结
- 17.错误处理
- 一、组合器
- (一)概念
- (二)常见组合器
- 二、自定义错误类型
- (一)实现`std::error::Error`特征
- (二)更详尽的错误类型
- 三、错误转换`From`特征
- (一)`From`特征介绍
- (二)实现`From`特征示例
- 四、归一化不同的错误类型
- (一)问题引入
- (二)解决方式
- 18.语言中的`unsafe`关键字
- 一、`unsafe`简介
- (一)存在原因
- (二)使用原则
- (三)安全保证
- 二、`unsafe`的超能力
- (一)解引用裸指针
- (二)调用`unsafe`或外部函数
- (三)访问或修改可变静态变量
- (四)实现`unsafe`特征
- (五)访问`union`中的字段
- 三、相关实用工具(库)
- 四、内联汇编(`asm!`宏)
- (一)基本用法
- (二)输入和输出
- (三)延迟输出操作数
- (四)显式指定寄存器
- (五)Clobbered寄存器
- 宏编程
- 一、宏的概述
- (一)宏的使用
- (二)宏的分类
- 二、宏和函数的区别
- (一)元编程
- (二)可变参数
- (三)宏展开
- (四)宏的缺点
- 三、声明式宏(`macro_rules!`)
- (一)基本概念
- (二)简化版的`vec!`宏
- (三)模式解析
- 四、过程宏
- (一)基本概念
- (二)自定义`derive`过程宏
- (三)类属性宏
- (四)类函数宏
- 19.异步编程async/await
- 一、Async编程简介
- (一)性能对比
- (二)async简介
- (三)async/.await简单入门
- 二、底层探秘: Future执行器与任务调度
- (一)Future特征
- (二)使用Waker来唤醒任务
- (三)构建一个定时器
- (四)执行器和系统IO
- 五、总结
- 20.异步编程:Pin、Unpin、async/await与Stream
- 一、Pin和Unpin
- (一)Pin的作用
- (二)为何需要Pin
- (三)Unpin
- (四)深入理解Pin
- (五)总结
- 二、async/await和Stream流处理
- (一)async/.await基础
- (二)当.await遇见多线程执行器
- (三)Stream流处理
- 21.异步编程进阶:同时运行多个Future
- 一、同时运行多个Future
- (一)join!宏
- (二)try_join!宏
- (三)select!宏
- 二、一些疑难问题的解决办法
- (一)在async语句块中使用?
- (二)async函数和Send特征
- (三)递归使用async fn
- (四)在特征中使用async
前言
这个笔记基于《The Rust Programming Language, 2nd Edition》 这本书为基础的记录学习笔记。有关这本书更多的详细可以网购或专卖店去详细了解,关于rust入门基础的文章有。
1. Rust生命周期进阶
一、不太聪明的生命周期检查
(一)例子1
#[derive(Debug)]
struct Foo;impl Foo {fn mutate_and_share(&mut self) -> &Self {&*self}fn share(&self) {}
}
fn main() {let mut foo = Foo;let loan = foo.mutate_and_share();foo.share();println!("{:?}", loan);
}
- 理论上
mutate_and_share
最终是不可变借用,share
也是不可变借用,应编译通过,但实际报错cannot borrow 'foo' as immutable because it is also borrowed as mutable
。 - 原因是生命周期消除规则使
mutate_and_share
中&mut self
和&self
生命周期相同,导致可变借用在main
函数作用域内有效,使share
无法再进行不可变借用。
(二)例子2
use std::collections::HashMap;
use std::hash::Hash;
fn get_default<'m, K, V>(map: &'m mut HashMap<K, V>, key: K) -> &'m mut V
whereK: Clone + Eq + Hash,V: Default,
{match map.get_mut(&key) {Some(value) => value,None => {map.insert(key.clone(), V::default());map.get_mut(&key).unwrap()}}
}
- 该代码不能编译,报错
cannot borrow '*map' as mutable more than once at a time
。 - 原因是编译器认为对
map
的可变借用持续到match
语句块结束,而实际在map.get_mut(&key)
调用完成后可变借用就可结束,导致后续借用失败。
二、无界生命周期
fn f<'a, T>(x: *const T) -> &'a T {unsafe {&*x}
}
- 不安全代码解引用裸指针时产生无界生命周期。
- 如上述代码中
x
无生命周期,&'a T
的'a
是无界生命周期,它不受约束,比'static
强大。 - 应在函数声明中运用生命周期消除规则避免无界生命周期。
三、生命周期约束(HRTB)
(一)语法及含义
'a: 'b
表示'a
至少要活得跟'b
一样久,如struct DoubleRef<'a,'b:'a, T> {r: &'a T, s: &'b T}
,'b
必须活得比'a
久。T: 'a
表示类型T
必须比'a
活得要久,如struct Ref<'a, T: 'a> {r: &'a T}
,新版本编译器可自动推导,可简化为struct Ref<'a, T> {r: &'a T}
。
(二)综合例子
struct ImportantExcerpt<'a> {part: &'a str,
}
impl<'a: 'b, 'b> ImportantExcerpt<'a> {fn announce_and_return_part(&'a self,announcement:&'b str) -> &'b str{println!("Attention please: {}", announcement);self.part}
}
- 需添加
'a: 'b
约束才能编译,因为self.part
生命周期与self
一致,'a
需转换为'b
且'a
>=b
。
四、闭包函数的消除规则
(一)问题示例
fn fn_elision(x: &i32) -> &i32 { x }
let closure_slision = |x: &i32| -> &i32 { x };
fn_elision
能编译,closure_slision
报错lifetime may not live long enough
,原因是编译器无法推测返回引用和传入引用谁活得更久。
(二)原因分析
- 函数生命周期体现在签名引用类型上,编译器可分析消除规则;闭包生命周期分散在参数和闭包体中,编译器难以分析,所以针对函数和闭包有不同消除规则。
(三)解决方法
fn fun<T, F: Fn(&T) -> &T>(f: F) -> F {f
}
- 可使用
Fn
特征解决,通过包装闭包解决生命周期问题。
五、NLL(Non - Lexical Lifetime)
(一)规则变化
- 旧规则:引用生命周期从借用开始到作用域结束;新规则(1.31版本引入):引用生命周期从借用处开始,到最后一次使用的地方结束。
(二)示例及分析
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;
- 按旧规则报错,按新规则
r1
和r2
在println!
后生命周期结束,r3
借用不违反规则。
六、Reborrow(再借用)
(一)概念及示例
#[derive(Debug)]
struct Point {x: i32,y: i32,
}
impl Point {fn move_to(&mut self, x: i32, y: i32) {self.x = x;self.y = y;}
}
fn main() {let mut p = Point { x: 0, y: 0 };let r = &mut p;let rr: &Point = &*r;println!("{:?}", rr);r.move_to(10, 10);println!("{:?}", r);
}
rr
是对r
的再借用,在rr
生命周期内不使用r
则不会报错。
(二)错误示例
let mut p = Point { x: 0, y: 0 };
let r = &mut p;
let rr: &Point = &*r;r.move_to(10, 10);println!("{:?}", rr);println!("{:?}", r);
- 在
rr
再借用生命周期内使用r
则会报错。
七、生命周期消除规则补充
(一)impl块消除
impl<'a> Reader for BufReader<'a> {// methods go here// impl内部实际上没有用到'a
}
- 如果
impl
块内部未用到'a
,可写成impl Reader for BufReader<'_>
,'_
是匿名生命周期,表示可忽略该生命周期。
(二)生命周期约束消除
// Rust 2015
struct Ref<'a, T: 'a> {field:&'a T
}// Rust 2018
struct Ref<'a, T> {field: &'a T
}
- 新版本Rust中,
T: 'a
可被消除,可显式声明但影响可读性。
八、复杂例子分析
(一)代码及错误
struct Interface<'a> {manager: &'a mut Manager<'a>
}
impl<'a> Interface<'a> {pub fn noop(self) {println!("interface consumed");}
}struct Manager<'a> {text: &'a str
}
struct List<'a> {manager: Manager<'a>,
}
impl<'a> List<'a> {pub fn get_interface(&'a mut self) -> Interface {Interface {manager: &mut self.manager}}
}
fn main() {let mut list = List {manager: Manager {text: "hello"}};list.get_interface().noop();println!("Interface should be dropped here and the borrow released");// 下面的调用会失败,因为同时有不可变/可变借用// 但是Interface在之前调用完成后就应该被释放了use_list(&list);
}
fn use_list(list: &List) {println!("{}", list.manager.text);
}
- 运行报错
cannot borrow 'list' as immutable because it is also borrowed as mutable
。
(二)原因分析
get_interface
方法中参数生命周期'a
与List
生命周期相同,导致可变借用持续到main
函数结束,无法再进行借用。
(三)解决方法
- 为
get_interface
方法的参数给予不同于List<'a>
的生命周期'b
。
struct Interface<'b, 'a: 'b>{manager:&'b mut Manager<'a>
}
impl<'b,'a:'b> Interface<'b,'a>{pub fn noop(self) {println!("interface consumed");}
}
struct Manager<'a>{text:&'a str
}
struct List<'a>{manager: Manager<'a>
}
impl<'a> List<'a>{pu fn get_interface<'b>(&'b mut self) -> Interface<'b,'a>where 'a:'b{Interface{manager:&'b mut self.manager}}
}
fn main() {let mut list = List {manager: Manager {text: "hello"}};list.get_interface().noop();println!("Interface should be dropped here and the borrow released");// 下面的调用可以通过,因为Interface的生命周期不需要跟list一样长use_list(&list);
}
fn use_list(list: &List) {println!("{}", list.manager.text);
}
2. &'static
和T: 'static
一、'static
的常见情况
(一)字符串字面量
let mark_twain: &str = "Samuel Clemens";
- 字符串字面量具有
'static
生命周期,它在程序的整个运行期间都存在。
(二)特征对象
- 特征对象的生命周期也是
'static
。
二、&'static
(一)含义
- 一个引用必须要活得跟剩下的程序一样久,才能被标注为
&'static
。
(二)示例及注意事项
fn get_memory_location() -> &str {let string = "Hello World";string
}
- 在上述代码中,字符串字面量“Hello World”的生命周期是
'static
,但变量string
的生命周期取决于函数作用域。 - 虽然
&'static
引用本身可以和程序活得一样久,但持有该引用的变量受其作用域的限制。
三、T: 'static
(一)约束情况
- 在某些情况下与
&'static
有相同的约束,即T
必须活得和程序一样久。
(二)示例及分析
1. 函数中直接使用T
fn print_it<T: Debug + 'static>(input: T) {println!("{:?}", input);
}fn main() {let i = 5;print_it(i);
}
- 这里直接使用
T
作为参数,当print_it
函数被调用时,&i
作为input
,由于i
的生命周期不是'static
,所以会报错。
2. 函数中使用&T
fn print_it<T: Debug + 'static>(input: &T) {println!("{:?}", input);
}
fn main() {let i = 5;print_it(&i);
}
- 在这个版本中,函数
print_it
接受&T
作为参数。使用&T
时,编译器不检查T
本身的生命周期,所以代码不会报错。
3. 另一个示例
fn static_bound<T: 'static>(input: &T) {println!("{:?}", input);
}
fn main() {let s1 = "String".to_string();static_bound(&s1);
}
- 这里
s1
虽然没有'static
生命周期,但是作为&T
,它满足T: 'static
的约束。
四、&'static
和T: 'static
的区别
(一)针对对象
&'static
针对引用,要求引用指向的数据活得跟程序一样久,而引用本身遵循作用域范围。T: 'static
主要约束类型T
,当使用其引用&T
时,编译器可能不检查T
本身的生命周期。
3.闭包笔记
一、闭包的定义和基本使用
(一)定义
- 闭包是一种匿名函数,它可以赋值给变量或作为参数传递给其他函数,并且能够捕获调用者作用域中的值。
let x = 1;
let sum = |y| x + y;
- 在上述代码中,
sum
闭包捕获了变量x
的值。 - 因此调用 sum(2) 意味着将 2(参数 y)跟 1(x)进行相加,最终返回它们的和:3。
(二)语法形式
- 闭包的形式为
|参数列表| 表达式
。
// 多个参数和多个语句的闭包
|param1, param2,...| {语句1;语句2;返回表达式
}
// 单个参数和单个返回表达式的闭包
|param1| 返回表达式
(三)类型推导
- Rust可以自动推导闭包的参数和返回值类型。但如果只声明了闭包而未使用,可能需要标注类型。与函数不同,闭包通常不对外作为 API,所以无需像函数那样手动标注类型供用户使用。
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
- 虽然类型推导很好用,但是它不是泛型,当编译器推导出一种类型后,它就会一直使用该类型:
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);// 报错:期待String类型,却发现一个整数
二、闭包用于简化代码
(一)传统函数实现问题
- 以健身代码为例,如果使用传统函数实现健身动作,当需要修改函数调用的声音或动作次数等内容时,需要在多处修改代码。
// 假设的健身动作函数
fn do_pushups() {println!("Doing pushups...");
}
fn main() {do_pushups();
}
(二)函数变量实现的不足
- 如果将函数赋值给变量后调用,虽然可以通过修改变量赋值来改变调用的函数,但如果函数内部参数发生变化,仍然需要在多处修改调用处的代码。
// 定义不同的健身动作函数
fn do_pushups() {println!("Doing pushups...");
}
fn do_situps() {println!("Doing situps...");
}
fn main() {let action = do_pushups;action();// 如果要修改为其他动作,只需修改 action 的赋值action = do_situps;action();
}
(三)闭包实现的优势
- 闭包可以捕获相关变量。例如在健身代码中,闭包
action
可以捕获intensity
,这样只需修改闭包内部的实现,而无需在多处修改调用处的代码。
let intensity = 3;
let action = |sound: &str| {for _ in 0..intensity {println!("{}", sound);}
};fn main() {action("Pushup!");intensity = 5;action("Situp!");
}
三、结构体中的闭包
(一)结构体设计
- 以简易缓存为例,设计
struct Cacher<T> where T: Fn(u32) -> u32 {query: T, value: Option<u32>}
。这里query
为闭包类型,受Fn(u32) -> u32
特征约束,表示闭包有一个u32
类型参数且返回u32
类型值。
struct Cacher<T> where T: Fn(u32) -> u32 {query: T,value: Option<u32>,
}
(二)方法实现
- 实现了
new
方法用于创建Cacher
实例,value
方法用于查询缓存值,如果不存在则调用闭包query
加载。
impl<T> Cacher<T> where T: Fn(u32) -> u32 {fn new(query: T) -> Cacher<T> {Cacher {query,value: None,}}fn value(&mut self, arg: u32) -> u32 {match self.value {Some(v) => v,None => {let v = (self.query)(arg);self.value = Some(v);v}}}
}
(三)局限性及改进方向
- 当前设计只支持
u32
类型的值,可以将u32
替换为泛型E
以支持更多类型。
struct Cacher<T,E>
whereT: Fn(E) -> E,E: Copy
{query: T,value: Option<E>,
}
impl<T,E> Cacher<T,E>
whereT: Fn(E) -> E,E: Copy
{fn new(query:T) -> Cacher<T,E>{Cacher {query,value: None,}}fn value(&mut self, arg:E) -> E{match self.value{Some(v)=>v,None=>{let v = (self.query)(arg);self.value = Some(v);v}}}
}
fn main(){}
#[test]
fn call_with_different_values(){let mut c = Cacher::new(|a|a);let v1 = c.value(1);let v2 = c.value(2);assert_eq!(v1,v2);;
}
四、闭包捕获作用域中的值
(一)闭包的特性
- 闭包可以捕获作用域中的值,而函数不能。
let x = 4;
let equal_to_x = |z| z == x;
- 在上述代码中,闭包
equal_to_x
可以使用x
的值。
(二)函数的限制
- 如果在函数中尝试访问作用域中的值,如
fn equal_to_x(z: i32) -> bool {z == x}
会报错,此时提示使用闭包替代。
error[E0434]: can't capture dynamic environment in a fn item // 在函数中无法捕获动态的环境--> src/main.rs:5:14|
5 | z == x| ^|= help: use the `|| { ... }` closure form instead // 使用闭包替代
(三)闭包对内存的影响
- 闭包从环境中捕获值时会分配内存存储,而函数不会,这在某些场景下可能会成为一种负担。
六、三种Fn
特征
(一)FnOnce
特征
- 闭包会拿走被捕获变量的所有权,只能运行一次。
fn fn_once<F>(func: F) where F: FnOnce(usize) -> bool {println!("{}", func(3));// 如果 F 未实现 Copy 特征,二次调用会报错// println!("{}", func(4));
}
- 可以使用
move
关键字强制闭包取得捕获变量的所有权,常用于闭包生命周期大于捕获变量生命周期的情况。
fn main() {let s = String::from("Hello");// 创建一个闭包,使用move关键字获取s的所有权let closure = move || println!("{}", s);// 在这里,s的作用域结束,但是闭包仍然可以使用它所拥有的s的副本// 因为闭包通过move关键字获取了所有权
}
(二)FnMut
特征
- 闭包以可变借用方式捕获环境中的值,可以修改该值。
let mut s = String::new();
let mut update_string = |str| s.push_str(str);
- 如果闭包未声明为可变会报错,需要使用
mut
关键字修饰闭包。即使闭包未用mut
修饰,但从特征类型系统和语言修饰符两方面可以保障程序正确运行。 - 例如,当
exec
函数接收可变类型闭包时,即使闭包看似不可变但实际是可变类型(由rust - analyzer
可看出实现了FnMut
特征)。闭包自动实现Copy
特征的规则是只要闭包捕获的类型都实现了Copy
特征,则闭包默认实现Copy
特征。例如,取得可变引用的闭包未实现Copy
特征。
fn main() {let mut s = String::new();let update_string = |str| s.push_str(str);exec(update_string);println!("{:?}",s);
}
fn exec<'a, F: FnMut(&'a str)>(mut f: F) {f("hello")
}
(三)Fn
特征
- 闭包以不可变借用方式捕获环境中的值。
let s = "hello, ".to_string();
let update_string = |str| println!("{},{}", s, str);
- 在闭包中只对
s
进行不可变借用。如果闭包实现的是FnMut
特征但在使用时标注为Fn
特征会报错,需要正确标注特征。
(四)三种特征的关系
- 所有闭包都自动实现
FnOnce
特征,至少可被调用一次。没有移出所捕获变量的所有权的闭包自动实现FnMut
特征,不需要对捕获变量进行改变的闭包自动实现Fn
特征。从特征约束看,Fn
的前提是实现FnMut
,FnMut
的前提是实现FnOnce
。Fn
获取&self
,FnMut
获取&mut self
,FnOnce
获取self
。在实际项目中,建议先使用Fn
特征,让编译器提示正误及如何选择。 - 示例 1: FnOnce
fn main() {let s = String::from("hello");// 使用 `move` 关键字创建闭包let move_closure = move || {println!("{}", s);};// 调用闭包move_closure();// 下面这行代码会导致编译错误,因为 `s` 的所有权已经转移到闭包中// println!("{}", s); // error: value borrowed here after move
}
- 示例 2: Fn
fn main() {let s = String::from("hello");// 创建一个只读闭包let read_closure = || {println!("{}", s);};// 调用闭包read_closure();// 再次调用闭包read_closure();// 输出结果println!("{}", s); // hello
}
- 示例 3: FnMut
fn main() {let mut s = String::from("hello");// 使用 `move` 关键字创建闭包let mut move_mut_closure = move || {s.push_str(", world");};// 调用闭包move_mut_closure();// 再次调用闭包move_mut_closure();// 下面这行代码会导致编译错误,因为 `s` 的所有权已经转移到闭包中// println!("{}", s); // error: value borrowed here after move
}
七、闭包作为函数返回值
(一)返回闭包的问题
- 最初尝试
fn factory() -> Fn(i32) -> i32 {...}
返回闭包会报错,因为闭包类型在编译时没有固定大小。
(二)解决方法
- 使用
impl Fn(i32) -> i32
作为返回类型可解决,但有局限,只能返回同样类型的闭包。若if
和else
分支返回不同闭包类型则报错。
(三)最终解决方案
- 使用
Box<dyn Fn(i32) -> i32>
作为返回类型,通过Box
方式将闭包装箱为特征对象来解决不同闭包类型返回的问题。
fn factory() -> Box<dyn Fn(i32) -> i32> {Box::new(|x| x + 1)
}
4.迭代器Interator
一、迭代器基础
(一)迭代器与for
循环
- 在Rust中,
for
循环是编译器提供的语法糖,用于遍历迭代器中的元素。 - 与其他语言(如JavaScript)的
for
循环不同,Rust的for
循环不依赖索引来访问集合中的元素。
// Rust中的for循环示例
let arr = [1, 2, 3];
for v in arr {println!("{}", v);
}
(二)IntoIterator
特征
- 实现
IntoIterator
特征的类型可以通过into_iter
方法转换为迭代器。 - 数组、数值序列等都实现了
IntoIterator
特征。
// 将数组转换为迭代器并遍历
let arr = [1, 2, 3];
for v in arr.into_iter() {println!("{}", v);
}
(三)迭代器的惰性初始化
- 迭代器是惰性的,创建迭代器时不会发生任何迭代行为,只有在使用时才会开始迭代。
// 创建迭代器但不立即迭代
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
// 在for循环中使用迭代器时才开始迭代
for val in v1_iter {println!("{}", val);
}
(四)next
方法
- 迭代器通过实现
Iterator
特征的next
方法来获取下一个元素。 next
方法返回Option
类型,有元素时返回Some(Item)
,无元素时返回None
。- 手动迭代时需要将迭代器声明为
mut
。
// 手动使用next方法遍历迭代器
let arr = [1, 2, 3];
let mut arr_iter = arr.into_iter();
assert_eq!(arr_iter.next(), Some(1));
assert_eq!(arr_iter.next(), Some(2));
assert_eq!(arr_iter.next(), Some(3));
assert_eq!(arr_iter.next(), None);
二、迭代器相关方法和类型转换
(一)into_iter
、iter
和ter_mut
into_iter
会夺走所有权,iter
是借用,iter_mut
是可变借用。
// into_iter示例
let values = vec![1, 2, 3];
for v in values.into_iter() {println!("{}", v);
}
// 由于所有权被夺走,下面代码会报错
// println!("{:?}", values);
// iter示例
let values = vec![1, 2, 3];
let values_iter = values.iter();
// 可以正常使用原集合
println!("{:?}", values);
// iter_mut示例
let mut values = vec![1, 2, 3];
let mut values_iter_mut = values.iter_mut();
if let Some(v) = values_iter_mut.next() {*v = 0;
}
// 输出修改后的集合
println!("{:?}", values);
(二)Iterator
和IntoIterator
的区别
Iterator
是迭代器特征,只有实现了它才能称为迭代器并调用next
方法。IntoIterator
强调一个类型实现该特征后可以通过into_iter
等方法变成一个迭代器。
三、消费者与适配器
(一)消费者
- 消费者是迭代器上依赖
next
方法消费元素并返回值的方法。 - 例如
sum
方法是消费者适配器,它会拿走迭代器的所有权并对元素进行求和。
// sum方法示例
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
// 由于所有权被拿走,下面代码会报错
// println!("{:?}", v1_iter);
(二)迭代器适配器
- 迭代器适配器返回一个新的迭代器,是实现链式方法调用的关键,且是惰性的,需要消费者适配器收尾。
- 例如
map
方法是迭代器适配器,collect
方法是消费者适配器。
// map和collect方法示例
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
(三)闭包作为适配器参数
- 闭包作为迭代器适配器的参数可以就地处理元素并能捕获环境值。
// 闭包作为filter迭代器适配器参数示例
struct Shoe {size: u32,style: String,
}fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
四、实现Iterator
特征
(一)自定义迭代器示例
- 可以为自定义类型实现
Iterator
特征来创建迭代器。 - 以
Counter
结构体为例,实现Iterator
特征并定义next
方法来实现计数逻辑。
// 定义Counter结构体
struct Counter {count: u32,
}
impl Counter {fn new() -> Counter {Counter { count: 0 }}
}
// 实现Iterator特征
impl Iterator for Counter {type Item = u32;fn next(&mut self) -> Option<Self::Item> {if self.count < 5 {self.count += 1;Some(self.count)} else {None}
}
// 使用自定义迭代器
let mut counter = Counter::new();
assert_eq!(counter.next(), Some(1));
assert_eq!(counter.next(), Some(2));
assert_eq!(counter.next(), Some(3));
assert_eq!(counter.next(), Some(4));
assert_eq!(job().next(), Some(5));
(二)Iterator
特征的其它方法
Iterator
特征中除next
外的方法有默认实现,且基于next
方法。- 例如
zip
、map
、filter
是迭代器适配器,sum
是消费者适配器,可以组合使用。 - zip:将两个迭代器合成一个迭代器,每次迭代返回一个元组
- map:对迭代器的每个元素应用一个函数,返回一个新的迭代器
- filter:给定的条件过滤迭代器中的元素,返回一个新的迭代器
- sum:计算迭代器中所有元素的总和,返回一个单一的值
// zip、map、filter和sum方法示例
let sum: u32 = Counter::new().zip(Counter::new().skip(1)).map(|(a, b)| a * b).filter(|x| x % 3 == 0).sum();
assert_eq!(18, sum);
五、enumerate
方法
enumerate
是迭代器适配器,它为迭代器元素添加索引,产生形如(索引,值)
的元组形式的新迭代器。
// enumerate方法示例
let v = vec![1u64, 2, 3, 4, 5, 6];
for (i, v) in v.iter().enumerate() {println!("第{}个值是{}", i, v);
}
六、迭代器的性能
- 通过测试,迭代器和
for
循环在完成求和任务时,迭代器性能更快一些。 - 迭代器是零成本抽象,不会引入运行时开销,编译器可进行优化。
5.类型转换
一、as
转换
(一)基本类型转换
- 使用
as
操作符进行基本类型转换。需要注意数据范围,避免将大类型转换为小类型导致错误。
fn main() {let a: i32 = 10;let b: u16 = 100;// 将b转换为i32类型才能比较if a < (b as i32) {println!("Ten is less than one hundred.");}let a = 3.1 as i8;let b = 100_i8 as i32;let c = 'a' as u8; // 将字符'a'转换为整数,97println!("{},{},{}",a,b,c)
}
(二)内存地址转换为指针
- 可以将内存地址转换为指针类型。
fn main() {let mut values: [i32; 2] = [1, 2];let p1: *mut i32 = values.as_mut_ptr();let first_address = p1 as usize; // 将p1内存地址转换为一个整数let second_address = first_address + 4; let p2 = second_address as *mut i32; unsafe {*p2 += 1;}assert_eq!(values[1], 3);
}
二、TryInto
转换
(一)使用场景
- 用于处理类型转换错误,相比
as
关键字有更多控制。
use std::convert::TryInto;fn main() {let a: u8 = 10;let b: u16 = 1500;let b_: u8 = b.try_into().unwrap();if a < b_ {println!("Ten is less than one hundred.");}
}
(二)错误处理
try_into
会返回Result
类型,可以对转换错误进行处理。
fn main() {let b: i16 = 1500;let b_: u8 = match b.try_into() {Ok(b1) => b1,Err(e) => {println!("{:?}", e.to_string());0}};
}
三、通用类型转换
(一)结构体转换示例
- 可以通过简单方式将一个结构体转换为另一个结构体,但存在更通用的方式。
struct Foo {x: u32,y: u16,
}
struct Bar {a: u32,b: u16,
}
fn reinterpret(foo: Foo) -> Bar {let Foo { x, y } = foo;Bar { a: x, b: y }
}
四、强制类型转换
(一)特征匹配
- 在匹配特征时,不会做强制转换(除了方法)。
trait Trait {}fn foo<X: Trait>(t: X) {}impl<'a> Trait for &'a i32 {}fn main() {let t: &mut i32 = &mut 0;foo(t);
}
(二)点操作符
- 方法调用的点操作符会发生类型转换,包括自动引用、自动解引用、强制类型转换直到类型匹配等。
fn do_stuff<T: Clone>(value: &T) {let cloned = value.clone();
}
- 上述代码中,编译器首先检查能否进行值方法调用,因为
T
实现了Clone
特征,所以可以进行值方法调用,cloned
的类型是T
。
五、变形记(Transmutes
)
(一)mem::transmute
- 非常危险的转换方式,将类型
T
直接转成类型U
,要求两个类型占用同样大小字节数。
fn foo() -> i32 {0
}
let pointer = foo as *const ();
let function = unsafe { // 将裸指针转换为函数指针std::mem::transmute::<*const (), fn() -> i32>(pointer)
};
assert_eq!(function(), 0);
- 可能导致创建任意类型实例混乱、重载返回类型、未定义行为(如将
&
变形为&mut
)等问题。
(二)mem::transmute_copy
- 比
mem::transmute
更危险,从T
类型中拷贝出U
类型所需字节数并转换,但不检查大小,U
尺寸大于T
时是未定义行为。
六、newtype
(一)定义和用途
- 使用元组结构体将已有类型包裹,可提供更有意义和可读性的类型名,解决某些场景问题,隐藏内部类型细节,为外部类型实现外部特征。
struct Meters(u32);
impl std::fmt::Display for Meters {fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {write!(f, "目标地点距离你{}米", self.0)}
}
(二)示例
- 为
Vec
实现Display
特征,使用newtype
Wrapper
包裹Vec
并实现Display
。
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {write!(f, "[{}]", self.0.join(", "))}
}
fn main() {let w = Wrapper(vec![String::from("hello"), String::from("world")]);println!("w = {}", w);
}
七、类型别名(Type Alias
)
(一)定义和特点
- 是某一个类型的别名,不是独立全新类型,编译器仍将其当作原类型使用。可简化代码,提高可读性,但不能实现为外部类型实现外部特征等功能。
- 类型别名并不是一个独立的全新的类型,而是某一个类型的别名
type Meters = u32;
let x: u32 = 5;
let y: Meters = 5;
println!("x + y = {}", x + y);
(二)应用场景
- 简化
Result<T, E>
枚举。
type Result<T> = std::result::Result<T, std::io::Error>;
八、!
永不返回类型
panic
的返回值是!
,代表函数永不返回任何值,可用于解决match
分支类型不匹配问题。
fn main() {let i = 2;let v = match i {0..=3 => i,_ => println!("不合规定的值:{}", i)};
}
九、Sized
和不定长类型DST
(一)不定长类型DST
- 包括动态大小数组、切片、
str
、特征对象等。这些类型大小在编译时无法得知,需通过间接方式使用。
fn my_function(n: usize) {let array = [123; n];
}
- 上述代码中,动态大小数组会报错,因为
n
在编译期无法得知。自然类型就变成了unized
。
str
它是一个动态类型,不是String动态字符串,也不三&str字符串切片,它同时还是String和&str的底层数据类型。在运行期才知道,所以在定义期间会报错:
// error
let s1: str = "Hello there!";
let s2: str = "How's it going?";
// ok
let s3: &str = "on?"
String和&str
底层堆数据的明确信息,它才是固定 大小类型,将动态类型数据固定化的秘决就是使用引用指向这些动态数据,然后在引用中存储相关的内存位置、长度等信息。
(二)Sized
特征
- 编译器自动为泛型函数添加
T: Sized
特征约束,保证泛型参数是固定类型。
fn generic<T: Sized>(t: T) {// --snip--
}
- 使用
?Sized
特征可在泛型函数中使用动态数据类型,此时函数参数类型需变为&T
。
fn generic<T:?Sized>(t: &T) {// --snip--
}
Box<str>
无法像封装特征对象那样简单封装str
,需使用into
方法让编译器自动转换。
十、整数转换为枚举
(一)C语言实现
- 在C语言中实现简单,通过
enum
定义和if
判断实现整数与枚举的匹配。
#include <stdio.h>
enum atomic_number {HYDROGEN = 1,HELIUM = 2,//...IRON = 26,
};
int main(void)
{enum atomic_number element = 26;if (element == IRON) {printf("Beware of Rust!\n");}return 0;
}
(二)Rust实现方式
- 使用三方库,如
num-traits
和num-derive
,通过FromPrimitive
特征实现转换。
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
#[derive(FromPrimitive)]
enum MyEnum {A = 1,B,C,
}
fn main() {let x = 2;match FromPrimitive::from_i32(x) {Some(MyEnum::A) => println!("Got A"),Some(MyEnum::B) => println!("Got B"),Some(MyEnum::C) => println!("Got C"),None => println!("Couldn't convert {}", x),}
}
- 在Rust 1.34后可实现
TryFrom
特征来做转换,也可使用宏简化TryFrom
特征的实现。 - 不推荐但可行的方式是使用
std::mem::transmute
,需注意底层类型大小控制。
6.Box<T>
堆对象分配
一、Rust中的堆栈
(一)堆栈概念
- 栈
- 内存从高位地址向下增长,连续分配,操作系统对其大小有限制(
main
线程栈大小为8MB
,普通线程为2MB
)。 - 函数调用时创建临时栈空间,调用结束自动进入
Drop
流程,栈顶指针自动移动,无需手动干预,申请和释放高效。
- 内存从高位地址向下增长,连续分配,操作系统对其大小有限制(
- 堆
- 内存从低位地址向上增长,通常只受物理内存限制,不连续。
- 堆上对象有所有者,受所有权规则限制,赋值时发生所有权转移(浅拷贝栈上引用或智能指针)。
(二)堆栈性能
- 小型数据:栈上分配和读取性能高于堆。
- 中型数据:栈上分配性能高,读取性能与堆无区别(无法利用寄存器或CPU高速缓存,需内存寻址)。
- 大型数据:建议在堆上分配和使用。
二、Box<T>
的使用场景
(一)将数据存储在堆上
fn main() {let a = Box::new(3);println!("a = {}", a); // 下面代码报错,需显式解引用// let b = a + 1; // 正确写法let b = *a + 1;
}
Box<T>
实现了Deref
和Drop
特征。println!
能正常打印是因为隐式调用Deref
解引用;let b = a + 1
报错,需用*
操作符显式解引用;a
的智能指针在main
函数结束时释放。
(二)避免栈上数据拷贝
fn main() {// 栈上数组let arr = [0;1000];let arr1 = arr;println!("{:?}", arr.len());println!("{:?}", arr1.len());// 堆上数组let arr = Box::new([0;1000]);let arr1 = arr;println!("{:?}", arr1.len());// 下面代码报错,因为arr不再拥有所有权// println!("{:?}", arr.len());
}
- 栈上数据转移所有权时拷贝数据,堆上转移所有权仅复制指针。
(三)将动态大小类型变为Sized固定大小类型
// 递归类型,报错
#![allow(unused)]
enum List {Cons(i32, List),Nil,
}// 使用Box<T>解决
#![allow(unused)]
enum List {Cons(i32, Box<List>),Nil,
}
- 递归类型是动态大小类型(DST),使用
Box<T>
可将其转换为固定大小类型。
(四)特征对象
trait Draw {fn draw(&self);
}
struct Button {id: u32,
}
impl Draw for Button {fn draw(&self) {println!("这是屏幕上第{}号按钮", self.id)}
}
struct Select {id: u32,
}
impl Draw for Select {fn draw(&self) {println!("这个选择框贼难用{}", self.id)}
}
fn main() {let elems: Vec<Box<dyn Draw>> = vec![Box::new(Button { id: 1 }), Box::for e in elems {e.draw()}
}
- 特征对象可将不同类型包装成实现某特征的对象放入数组,特征是DST类型,特征对象将其转换为固定大小类型,
Box<dyn Drew>
就是特征对象。
三、Box
内存布局
(一)Vec<i32>
内存布局
#![allow(unused)]
fn main() {
(stack) (heap)
┌──────┐ ┌───┐
│ vec1 │──→│ 1 │
└──────┘ ├───┤│ 2 │├───┤│ 3 │├───┤│ 4 │└───┘
}
Vec
智能指针存储在栈中,指向堆上数组数据。
(二)Vec<Box<i32>>
内存布局
#![allow(unused)]
fn main() {(heap)
(stack) (heap) ┌───┐
┌──────┐ ┌───┐ ┌─→│ 1 │
│ vec2 │──→│B1 │─┘ └───┘
└──────┘ ├───┤ ┌───┘│B2 │───→│ 2 │├───┤ └───┘│B3 │─┐ ┌───┘├───┤ └─→│ 3 ││B3 │─┐ └───┘└───┘ │ ┌───┘└─→│ 4 │└───┘
}
- 智能指针
vec2
存储在栈上,指向堆上数组,数组元素是Box
智能指针,Box
又指向实际值。 - 从数组取元素时,需对
Box
解引用,如let arr = vec![Box::new(1), Box::new(2)]; let (first, second) = (&arr[0], &arr[1]); let sum = **first + **second;
。
⭕️注意:
- 使用
&
借用数组中的元素,否则会报所有权错误。 - 表达式不能隐式的解引用,因此必须使用
**
做两次解引用,第一次将&Box<i32>
类型转成Box<i32>
,第二次进一步转成i32
四、Box::leak
(一)功能
- 消费
Box
并强制目标值从内存中泄漏,可将String
类型转为'static
生命周期的&str
类型。
(二)示例
fn main() {let s = gen_static_str();println!("s = {}", s);
}
fn gen_static_str() -> &'static str{let mut s = String::new();s.push_str("hello, world");Box::leak(s.into_boxed_str())
}
- 使用场景:运行期初始化的值需全局有效时可使用,如存储配置的结构体实例在运行期动态插入内容后转为全局有效,
Box::leak
性能高于Rc/Arc
。 - 要知道真正具有
'static
生命周期的往往都是编译期就创建的值,被打包到二进制可执行文件中,在整个程序中存活得都一样久。
7.Deref
解引用
一、Deref
的引入
(一)问题示例
#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Person {name: String,age: u8
}
impl Person {fn new(name: String, age: u8) -> Self {Person { name, age}}fn display(self: &mut Person, age: u8) {let Person{name, age} = &self;}
}
}
- 在
display
方法中,&mut Person
类型的self
取引用后为&&mut Person
,却能与Person
类型匹配,这是因为Deref
特征的作用。
(二)智能指针与Deref
- 智能指针实现了
Deref
和Drop
特征。 Deref
让智能指针像引用一样工作,可通过*
操作符解引用,如Box<T>
智能指针。Drop
用于指定智能指针超出作用域后自动执行的代码。
二、常规引用解引用
fn main() {let x = 5;let y = &x;assert_eq!(5, x);assert_eq!(5, *y);
}
y
是常规引用,包含x
的内存地址,通过*y
可获取x
的值。若执行assert_eq!(5, y);
会报错,因为无法将引用与数值比较。
三、智能指针解引用
(一)自定义智能指针
#![allow(unused)]
struct MyBox<T>(T);
impl<T> MyBox<T> {fn new(x: T) -> MyBox<T> {MyBox(x)}
}
- 定义了一个类似
Box<T>
的智能指针MyBox<T>
,通过MyBox::new
创建。
(二)为自定义智能指针实现Deref
特征
#![allow(unused)]
use std::ops::Deref;
impl<T> Deref for MyBox<T> {type Target = T;fn deref(&self) -> &Self::Target {&self.0}
}
- 实现
Deref
特征后,MyBox
智能指针可通过*
解引用,返回元组结构体中的元素&self.0
。
(三)*
背后的原理
- 对智能指针
Box
解引用时,实际调用*(y.deref())
,先调用deref
方法返回常规引用,再通过*
解引用获取目标值。 - 这样做是因为所有权系统,若
deref
直接返回值会转移所有权,而我们不希望这样。
四、函数和方法中的隐式Deref
转换
(一)基本隐式转换
fn main() {let s = String::from("hello world");display(&s)
}
fn display(s: &str) {println!("s = {}", s);
}
String
实现了Deref
特征,&s
(&String
类型)传给display
函数时自动转换为&str
。
(二)连续隐式转换
fn main() {let s = MyBox::new(String::from("hello world"));display(&s)
}
fn display(s: &str) {println!("s = {}", s);
}
- 使用自定义智能指针
MyBox
,通过连续隐式转换变成&str
类型:先Deref
成String
,再Deref
成&str
。
(三)在方法、赋值中的应用
fn main() {let s = MyBox::new(String::from("hello, world"));let s1: &str = &s;let s2: String = s.to_string();
}
- 对于
s1
,通过两次Deref
将&str
类型的值赋给它;对于s2
,MyBox
虽未实现to_string
方法,但通过Deref
可调用该方法。
五、Deref
规则总结
(一)基本规则
- 若
T: Deref<Target=U>
,则&foo
(foo
为T
类型对象)会自动转换为&U
。
(二)引用归一化
- Rust会对智能指针和多重
&
引用做引用归一化操作,转换成&v
形式再解引用。 - 例如,
&&&&T
会自动解引用为&&T
,然后&&T
再自动解引用为&T
。
六、三种Deref
转换
rust支持将一个可变的引用转换成另一个可变的引用,将一个可变引用转换成一个不可变的引用:
(一)不可变Deref
转换
- 当
T: Deref<Target=U>
,可将&T
转换成&U
。
(二)可变Deref
转换
- 当
T: DerefMut<Target=U>
,可将&mut T
转换成&mut U
。
(三)可变转不可变Deref
转换
- 当
T: Deref<Target=U>
,可将&mut T
转换成&U
,但反之不行。
(四)示例
struct MyBox<T> {v: T,
}
impl<T> MyBox<T> {fn new(x: T) -> MyBox<T> {MyBox { v: x }}
}
use std::ops::Deref;
impl<T> Deref for MyBox<T> {type Target = T;fn deref(&fn display(s: &mut String) {s.push_str("world");println!("s = {}", s);
}
- 实现
DerefMut
必须先实现Deref
特征。 T: DerefMut<Target=U>
将&mut T
类型转换为&mut U
类型。
七、总结
Deref
是Rust中常见的隐式类型转换,可连续实现如Box<String> -> String -> &str
的隐式转换。- 原则上只应为自定义智能指针实现
Deref
特征。
8.Drop`释放资源
一、Drop
特征的作用
- 在Rust中,
Drop
特征用于自动和手动释放资源及执行指定的收尾工作。它是智能指针的必备特征之一,使得Rust无需GC和手动资源回收。
二、Drop
示例
(一)结构体的Drop
实现
struct HasDrop1;
struct HasDrop2;
impl Drop for HasDrop1 {fn drop(&mut self) {println!("Dropping HasDrop1!");}
}
impl Drop for HasDrop2 {fn drop(&mut self) {println!("Dropping HasDrop2!");}
}
struct HasTwoDrops {one: HasDrop1,two: HasDrop2,
}
impl Drop for HasTwoDrops {fn drop(&mut self) {println!("Dropping HasTwoDrops!");}
}
struct Foo;
impl Drop for Foo {fn drop(&mut self) {println!("Dropping Foo!")}
}
fn main() {let _x = HasTwoDrops {two: HasDrop2,one: HasDrop1,};let _foo = Foo;println!("Running!");
}
- 每个结构体都可以实现
Drop
特征,其中drop
方法借用目标的可变引用。 - 输出结果为
Running!
、Dropping Foo!
、Dropping HasTwoDrops!
、Dropping HasDrop1!
、Dropping HasDrop2!
,符合Drop
顺序规则:变量级别按逆序,结构体内部按顺序。
(二)未实现Drop
的结构体
- 即使结构体未实现
Drop
特征,其内部字段若实现了Drop
,仍会调用drop
方法。
三、Drop
顺序
- 变量级别:按照逆序方式,先创建的变量后被
drop
。 - 结构体内部:按照定义顺序依次
drop
。
四、手动回收
(一)手动drop
的问题
#[derive(Debug)]
struct Foo;impl Drop for Foo {fn drop(&mut self) {println!("Dropping Foo!")}
}
fn main() {let foo = Foo;foo.drop();println!("Running!:{:?}", foo);
}
- 直接调用
Drop
特征的drop
方法是不允许的,会报错。因为Drop::drop
只是借用可变引用,提前调用drop
后代码仍可能访问不存在的值,不安全。
(二)正确的手动drop
方式
fn main() {let foo = Foo;drop(foo);// 以下代码会报错:借用了所有权被转移的值// println!("Running!:{:?}", foo);
}
- 应使用
std::mem::drop
函数进行手动drop
,它会拿走目标值的所有权,保证后续使用会导致编译错误,从而保证安全。
五、Drop
使用场景
- 回收内存资源:在绝大多数情况下,Rust会自动回收内存资源,但对于文件描述符、网络socket等特殊资源,超出作用域时需手动
drop
以释放资源。 - 执行收尾工作:如在结构体中实现
Drop
特征,可在drop
方法中执行一些自定义的收尾工作。
六、Copy
和Drop
的互斥
#![allow(unused)]
#[derive(Copy)]
struct Foo;
impl Drop for Foo {fn drop(&mut self) {println!("Dropping Foo!")}
}
- 一个类型不能同时实现
Copy
和Drop
特征,因为实现Copy
的类型会被隐式复制,难以预测析构函数执行时间和频率。
9.智能指针Rc、Arc、Cell、RefCell
一、Rc与Arc
(一)引入原因
- Rust所有权机制要求一个值只有一个所有者,但在某些场景下会有问题,比如图数据结构中多个边可能拥有同一个节点,多线程中多个线程可能持有同一个数据但无法同时获取可变引用。所以引入Rc和Arc来实现多个所有者共享一个数据,Rc适用于单线程,Arc适用于多线程。
(二)Rc
- 基本原理
- Rc是引用计数(reference counting)的英文缩写,它通过记录一个数据被引用的次数来确定该数据是否正在被使用。当引用次数归零时,就代表该数据不再被使用,因此可以被清理释放。
- 它适用于在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束的情况。
- 使用示例
use std::rc::Rc;
fn main() {let a = Rc::new(String::from("hello, world"));let b = Rc::clone(&a);assert_eq!(2, Rc::strong_count(&a));assert_eq!(Rc::strong_count(&a), Rc::strong_count(&b))
}
- 如上述代码,使用
Rc::new
创建一个新的Rc<String>
智能指针并赋给变量a
,此时引用计数为1。然后使用Rc::clone
克隆一份智能指针Rc<String>
并赋给b
,引用计数增加到2。a
和b
共享底层的字符串数据,clone
操作只是复制智能指针并增加引用计数,并没有克隆底层数据,是一种高效的浅拷贝。
- 引用计数变化
- 可以使用关联函数
Rc::strong_count
来获取当前引用计数的值。例如:
- 可以使用关联函数
use std::rc::Rc;
fn main() {let a = Rc::new(String::from("test ref counting"));println!("count after creating a = {}", Rc::strong_count(&a));let b = Rc::clone(&a);println!("count after creating b = {}", Rc::strong_count(&a));{let c = Rc::clone(&a);println!("count after creating c = {}", Rc::strong_count(&c));}println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
- 变量
c
在语句块内部声明,当离开语句块时它会因为超出作用域而被释放,所以引用计数会减少1。当a
、b
超出作用域后,引用计数会变成0,最终智能指针和它指向的底层字符串都会被清理释放。
- 不可变引用特性
Rc<T>
是指向底层数据的不可变的引用,因此无法通过它来修改数据。如果要修改数据,需要配合其它数据类型如RefCell<T>
或Mutex<T>
。
- 综合例子
- 考虑一个场景,有很多小工具,每个工具都有自己的主人,但是存在多个工具属于同一个主人的情况,此时使用
Rc<T>
就非常适合。
- 考虑一个场景,有很多小工具,每个工具都有自己的主人,但是存在多个工具属于同一个主人的情况,此时使用
use std::rc::Rc;
struct Owner {name: String,//...其它字段
}
struct Gadget {id: i32,owner: Rc<Owner>,//...其它字段
}
fn main() {// 创建一个基于引用计数的 `Owner`.let gadget_owner: Rc<Owner> = Rc::new(Owner {name: "Gadget Man".to_string(),});// 创建两个不同的工具,它们属于同一个主人let gadget1 = Gadget {id: 1,owner: Rc::clone(&gadget_owner),};let gadget2 = Gadget {id: 2,owner: Rc::clone(&gadget_owner),};// 释放掉第一个 `Rc<Owner>`drop(gadget_owner);// 尽管在上面我们释放了 gadget_owner,但是依然可以在这里使用 owner 的信息// 原因是在 drop 之前,存在三个指向 Gadget Man 的智能指针引用,上面仅仅// drop 掉其中一个智能指针引用,而不是 drop 掉 owner 数据,外面还有两个// 引用指向底层的 owner 数据,引用计数尚未清零// 因此 owner 数据依然可使用println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);// 在函数最后,`gadget1` 和 `gadget2` 也被释放,最终引用计数归零,随后底层// 数据也被清理释放
}
- Rc在多线程中的问题
- 在多线程场景中使用
Rc<T>
会报错,因为它不能在线程间安全的传递,实际上是因为它没有实现Send
特征,且计数器没有使用任何并发原语,无法实现原子化的计数操作,最终会导致计数错误。
- 在多线程场景中使用
(三)Arc
- 基本原理
- Arc是
Atomic Rc
的缩写,是原子化的Rc<T>
智能指针。原子化是一种并发原语,它能保证我们的数据能够安全的在线程间共享。
- Arc是
- 性能损耗
- 原子化或者其它锁虽然可以带来线程安全,但都会伴随着性能损耗,而且这种性能损耗还不小。所以Rust把这种选择权交给开发者,因为需要线程安全的代码其实占比并不高,大部分时候我们开发的程序都在一个个线程内。
- 与Rc的区别
- Arc和Rc拥有完全一样的API,修改起来很简单,只需要把
use std::rc::Rc
改为use std::sync::Arc
。两者的区别在于Arc是线程安全的,可以用于多线程中共享数据,而Rc只能用于同一线程内部。
- Arc和Rc拥有完全一样的API,修改起来很简单,只需要把
二、Cell与RefCell
(一)引入原因
- Rust编译器严格的所有权和借用规则在某些场景下缺乏灵活性,Cell和RefCell用于内部可变性,即可以在拥有不可变引用的同时修改目标数据。
(二)Cell
- 基本原理
Cell<T>
适用于T
实现Copy
的情况。它通过get
方法取值,set
方法设置新值。
- 使用示例
use std::cell::Cell;
fn main() {let c = Cell::new("asdf");let one = c.get();c.set("qwer");let two = c.get();println!("{},{}", one, Two types cannot be used as an index to a tuple, consider using a named tuple or a struct instead.和two);
}
- 局限性
- 如果尝试在
Cell
中存放未实现Copy
的类型(如String
),编译器会立刻报错。
- 如果尝试在
(三)RefCell
- 基本原理
RefCell<T>
用于引用类型,它将借用规则从编译期推迟到程序运行期。违背规则会导致运行时panic
。
- 使用示例
use std::cell::RefCell;
fn main() {let s = RefCell::new(String::from("hello, world"));let s1 = s.borrow();let s2 = s.borrow_mut();println!("{},{}", s1, s2);
}
- 上述代码在编译期不会报任何错误,但运行时会因为违背了借用规则导致
panic
。
- 存在意义
- 用于编译器误判或引用在多处使用修改难以管理借用关系的情况。
- 性能比较
RefCell
有一点运行期开销,因为它包含了一个字节大小的“借用状态”指示器,该指示器在每次运行时借用时都会被修改,进而产生一点开销。
(四)内部可变性
- 概念
- 内部可变性是指对一个不可变的值进行可变借用,这不符合Rust的基本借用规则。例如:
fn main() {let x = 5;let y = &mut x;
}
- 上述代码会报错,因为不能对一个不可变的值进行可变借用。但在某些场景中,一个值可以在其方法内部被修改,同时对于其它代码不可变,是很有用的。
- 示例
use std::cell::RefCell;
pub trait Messenger {fn send(&self, msg: String);
}
pub struct MsgQueue {msg_cache: RefCell<Vec<String>>,
}
impl Messenger for MsgQueue {fn send(&self, msg: String) {self.msg_cache.borrow_mut().push(msg)}
}
fn main() {let mq = MsgQueue {msg_cache: RefCell::new(Vec::new()),};mq.send("hello, world".to_string());
}
- 在实现
Messenger
特征的send
方法中,通过RefCell
包裹msg_cache
实现对不可变引用中数据的修改。
(五)Rc + RefCell组合使用
- 使用示例
use std::cell::RefCell;
use std::rc::Rc;
fn main() {let s = Rc::new(RefCell::new("我很善变,还拥有多个主人".to_string()));let s1 = s.clone();let s2 = s.clone();// let mut s2 = s.borrow_mut();s2.borrow_mut().push_str(", oh yeah!");println!("{:?}\n{:?}\n{:?}", s, s1, s2);
}
- 上述代码中,使用
RefCell<String>
包裹一个字符串,同时通过Rc
创建了它的三个所有者:s
、s1
和s2
,并且通过其中一个所有者s2
对字符串内容进行了修改。由于Rc
的所有者们共享同一个底层的数据,因此当一个所有者修改了数据时,会导致全部所有者持有的数据都发生了变化。
- 性能分析
- 内存损耗:从对内存的影响来看,仅仅多分配了三个
usize/isize
,并没有其它额外的负担。 - CPU损耗:
- 对
Rc<T>
解引用是免费的(编译期),但是*
带来的间接取值并不免费。 - 克隆
Rc<T>
需要将当前的引用计数跟0
和usize::Max
进行一次比较,然后将计数值加1。 - 释放(
drop
)Rc<T>
需要将计数值减1, 然后跟0
进行一次比较。 - 对
RefCell
进行不可变借用,需要将isize
类型的借用计数加1,然后跟0
进行比较。 - 对
RefCell
的不可变借用进行释放,需要将isize
减1。 - 对
RefCell
的可变借用大致流程跟上面差不多,但是需要先跟0
比较,然后再减1。 - 对
RefCell
的可变借用进行释放,需要将isize
加1。
- 对
- CPU缓存:可能不够亲和,但需要在实际场景中进行测试。
- 内存损耗:从对内存的影响来看,仅仅多分配了三个
(六)通过Cell::from_mut解决借用冲突
- 问题示例
#![allow(unused)]
fn main() {fn is_even(i: i32) -> bool {i % 2 == 0}fn retain_even(nums: &mut Vec<i32>) {let mut i = 0;for num in nums.iter().filter(|&num| is_even(*num)) {nums[i] = *num;i += 1;}nums.truncate(i);}
}
- 上述代码会报错,因为同时借用了不可变与可变引用。
- 解决方法
#![allow(unused)]
fn main() {use std::ell::Cell;fn retain_even(nums: &mut Vec<i32>) {let slice: &[Cell<i32>] = Cell::from_mut(&mut nums[..])-> as_slice_of_cells();let mut i = 0;for num in slice.iter().filter(|num| is_even(num.get())) {slice[i].set(num.get());i += 1;}nums.truncate(i);}
}
- 使用
Cell::from_mut
和Cell::as_slice_of_cells
方法将&mut [T]
转换为&[Cell<T>]
解决问题。
10.循环引用与结构体自引用
一、循环引用
(一)循环引用的产生
- 示例代码分析
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;#[derive(Debug)]
enum List {Cons(i32, RefCell<Rc<List>>),Nil,
}
impl List {fn tail(&self) -> Option<&RefCell<Rc<List>>> {match self {Cons(_, item) -> Some(item),Nil -> None,}}
}
fn main() {let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));if let Some(link) = a.tail() {*link.borrow_mut() = Rc::clone(&b);}
}
- 上述代码定义了
List
枚举类型,其中Cons
变体包含i32
和RefCell<Rc<List>>
。在main
函数中,首先创建了a
,然后基于a
创建了b
,接着通过RefCell
的可变性让a
和b
相互引用,形成循环引用。 - 引用计数问题
// 以下是在main函数中添加的打印引用计数的代码
println!("a的初始化rc计数 = {}", Rc::strong_count(&a));
println!("在b创建后,a的rc计数 = {}", Rc::strong_count(&a));
println!("b的初始化rc计数 = {}", Rc::strong_count(&b));
println!("在更改a后,b的rc计数 = {}", Rc::strong_count(&b));
println!("在更改a后,a的rc计数 = {}", Rc::strong_count(&a));
- 循环引用导致
a
和b
的引用计数在main
函数结束前均为2
,不会归零。例如,a
的初始化rc
计数为1
,b
创建后a
的rc
计数变为2
,b
的初始化rc
计数为1
,经过一些操作后a
和b
的计数最终都为2
。 - 导致的问题
- 循环引用可能会使程序不断分配内存、泄漏内存,最终导致
OOM
。如果尝试打印循环引用的内容,还可能造成栈溢出,如下所示:
- 循环引用可能会使程序不断分配内存、泄漏内存,最终导致
// 反注释这行代码会导致栈溢出
// println!("a next item = {:?}", a.tail());
(二)Weak解决循环引用
- Weak的特点
Weak
类似于Rc
,但不持有所有权,不增加引用计数,仅保存指向数据的弱引用。通过upgrade
方法访问数据,返回Option<Rc<T>>
,若引用的值不存在则返回None
。
- 与Rc的对比
Weak | Rc |
---|---|
不计数 | 引用计数 |
不拥有所有权 | 拥有值的所有权 |
不阻止值被释放(drop) | 所有权计数归零,才能drop |
引用的值存在返回Some ,不存在返回None | 引用的值必定存在 |
通过upgrade 取到Option<Rc<T>> ,然后再取值 | 通过Deref 自动解引用,取值无需任何操作 |
- 使用示例
- 工具间的故事
use std::rc::Rc;
use std::rc::Weak;
use std::cell::RefCell;struct Owner {name: String,gadgets: RefCell<Vec<Weak<Gadget>>>,
}
struct Gadget {id: i32,owner: Rc<Owner>,
}
fn main() {let gadget_owner: Rc<Owner> = Rc::new(Owner {name: "Gadget Man".to_string(),gadgets: RefCell::new(Vec::new()),});let gadget1 = Rc::new(Gadget{id: 1, owner: gadget_owner.clone()});let gadget2 = Rc::new(Gadget{id: 2, owner: gadget_owner.clone()});gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(&gadget1));gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(&gadget2));for gadget_opt in gadget_owner.gadgets.borrow().iter() {let gadget = gadget_opt.upgrade().unwrap();println!("Gadget {} owned by {}", gadget.id, gadget.owner.name);}
}
- 在这个例子中,
Owner
结构体包含RefCell<Vec<Weak<Gadget>>>
,Gadget
结构体包含Rc<Owner>
,通过Weak
避免循环引用。 - tree数据结构
use std::cell::RefCell;
use std::rc::{Rc, Weak};#[derive(Debug)]
struct Node {value: i32,parent: RefCell<Weak<Node>>,children: RefCell<Vec<Rc<Node>>>,
}
fn main() {let leaf = Rc::new(Node {value: 3,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![]),});{let branch = Rc::new(Node {value: 5,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![Rc::clone(&leaf)]),});*leaf.parent.borrow_mut() = Rc::downgrade(&branch);}println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
- 在此例中,
Node
结构体包含RefCell<Weak<Node>>
(父节点引用)和RefCell<Vec<Rc<Node>>>
(子节点引用),通过Weak
和Rc
的配合避免循环引用。
(三)unsafe解决循环引用
- 方法介绍
- 可以使用
unsafe
里的裸指针解决循环引用问题,虽然不安全但性能高、代码简单符合直觉。
- 可以使用
- 示例说明
- 文中未详细展开,只提供了相关源码链接。
二、结构体自引用
(一)自引用结构体的问题
- 简单示例及报错
#![allow(unused)]
fn main() {struct SelfRef<'a> {value: String,pointer_to_value: &'a str,}
}
fn main() {let s = "aaa".to_string();let v = SelfRef {value: s,pointer_to_value: &s};
}
- 上述代码定义了
SelfRef
结构体,包含String
和指向该String
的&'a str
引用。在使用时会报错,因为试图同时使用值和值的引用,导致所有权转移和借用冲突。 - Option方法的限制
#[derive(Debug)]
struct WhatAboutThis<'a> {name: String,nickname: Option<&'a str>,
}
fn creator<'a>() -> WhatAboutThis<'a> {let mut tricky = WhatAboutThis {name: "Annabelle".toString(),nickname: None,};tricky.nickname = Some(&tricky.name[..4]);tricky
}
- 使用
Option
可以部分解决问题,但存在限制。例如从函数创建并返回包含自引用的结构体是不可能的,上述代码会报错。
(二)unsafe实现自引用
- 方法介绍
- 在结构体中直接存储裸指针代替引用,不受借用规则和生命周期限制,但通过指针获取值时需使用
unsafe
代码。
- 在结构体中直接存储裸指针代替引用,不受借用规则和生命周期限制,但通过指针获取值时需使用
- 示例说明
#[derive(Debug)]
struct SelfRef {value: String,pointer_to_value: *const String,
}
impl SelfRef {fn new(txt: np - Unresolved reference 'np'. Should be a valid Python identifier.): Self {SelfRef {value: String::from(txt),pointer_to_value: std::ptr::null(),}}fn init(&mut self) {let self_ref: *const String = &self.value;self.pointer_to_value = self_ref;}fn value(&self) -> &str {&self.value}fn pointer_to_value(&self) -> &String {assert!(!self.pointer_to_value.is_null(), "Test::b called without Test::init being called first");unsafe { &*(self.pointer_to_value) }}
}fn main() {let mut t = SelfRef::new("hello");t.init();println!("{}, {:p}", t.value(), t.pointer_to_value());
}
- 上述代码定义了
SelfRef
结构体,包含String
和*const String
指针。通过init
方法初始化指针,在pointer_to_value
方法中通过unsafe
获取指针指向的值。
(三)Pin实现自引用
- Pin的作用
Pin
可以固定住一个在模块内,模块的全局变量可以通过模块名.变量名的方式访问。一个值,防止在内存中被移动,可用于解决自引用结构体创建引用时所有权转移的问题。
- 示例说明
use std::marker::PhantomPinned;
use std::pin::Pin;
use std::ptr::NonNull;struct Unmovable {data: String,slice: NonNull<String>,_pin: PhantomPinned,
}impl Unmovable {fn new(data: String) -> Pin<Box<Self>> {let res = Unmovable {data,slice: NonNull::dangling(),_pin: PhantomPinned,};let mut boxed = Box::pin(res);let slice = NonNull::from(&boxed.data);unsafe {let mut_ref: Pin<&mut Self> = Pin::as_mut(&mut boxed);Pin::get_unchecked_mut(mut_ref).slice = slice;}boxed}
}fn main() {let unmoved = Unmovable::new("hello".toString());let mut still_unmoved = unmoved;assert_eq!(still_unmoved.slice, NonNull::from(&still_unmoved.data));
}
- 上述代码定义了
Unmovable
结构体,包含String
、指向String
的NonNull<String>
指针和PhantomPinned
。通过Pin<Box<Self>>
确保数据所有权不会被转移,在new
方法中正确设置指针。
(四)ouroboros库实现自引用
- 使用方法
- 使用
ouroboros
库时,需要按照其方式创建结构体和引用类型,如SelfRef
变成SelfRefBuilder
,引用字段从pointer_to_value
变成pointer_to_value_builder
。通过borrow_value
和borrow_pointer_to_value
方法借用值和指针。
- 使用
- 示例说明
use ouroboros::self_referencing;#[self_referencing]
struct SelfRef {value: String,#[borrows(value)]pointer_to_value: &'this str,
}fn main() {let v = SelfRefBuilder {value: "aaa".toString(),pointer_to_value_builder: |value: &String| value,}.build();let s = v.borrow_value();let p = v.borrow_pointer_to_value();assert_eq!(s, *p);
}
- 限制说明
- 该库有一定限制,如不适合
Vec
动态数组,因为数组内存地址可能改变,且对修改某些数据类型有限制。
- 该库有一定限制,如不适合
(五)其他相关库
- rental库
- 比较有名但可能不再维护,是三个库中最强大的,网上用例较多。
- owning-ref库
- 将所有者和它的引用绑定到一个封装类型。
(六)Rc + RefCell或Arc + Mutex解决自引用
- 方法介绍
- 类似于循环引用的解决方式,但会导致代码类型标识复杂,影响可读性。
(七)终极大法
- 思路介绍
- 如果两个相关部分放在一起会报错,就分开它们,但会增加代码复杂度。
(八)学习资源推荐
- 书籍推荐
- 推荐《Learn Rust by writing Entirely Too Many Linked Lists》,专门讲如何实现链表,涉及自引用相关知识。
11.并发与线程
一、并发和并行
(一)概念区别
- 并发(Concurrent)
- 比喻解释:多个队列使用同一个咖啡机,队列轮换使用,最终每个人都能接到咖啡。
- 实际情况:在单核心CPU时,多线程任务队列通过操作系统的任务调度,快速轮换处理不同任务,给用户所有任务同时运行的假象。例如,当某个任务执行时间过长,调度器会切换任务,实现表面上的多任务同时处理,但实际只有一个CPU核心在工作。
- 正式定义:系统支持两个或多个动作同时“存在”,在单核处理器上运行时线程交替换入或换出内存。
- 并行(Parallel)
- 比喻解释:每个队列都拥有一个咖啡机,最终每个人都能接到咖啡,且效率更高,因为同时可以有两个人在接咖啡。
- 实际情况:在多核心CPU时,每个核心可同时处理一个任务,提高效率。
- 正式定义:系统支持两个或多个动作同时“执行”,在多核处理器上运行时线程分配到独立处理器核上同时运行。
- 关系:并行是并发概念的子集,编写的并发程序在多核处理器上才能以并行方式运行。
(二)编程语言的并发模型
- 1:1线程模型
- 如Rust,直接调用操作系统创建线程的API,程序内线程数和占用操作系统线程数相等。
- M:N线程模型
- 如Go语言,内部实现自己的线程模型,程序内部的M个线程以某种映射方式使用N个操作系统线程。
二、使用线程
(一)多线程编程的风险
- 竞态条件:多个线程以非一致性的顺序同时访问数据资源。
- 死锁:两个线程都想使用某个资源,但都在等待对方释放资源后才能使用,结果最终都无法继续执行。
- 隐晦的BUG:一些因为多线程导致的很隐晦的BUG,难以复现和解决。
(二)创建线程
use std::thread;
use std::time::Duration;fn main() {thread::spawn(|| {for i in 1..10 {println!("hi number {} from the spawned thread!", i);thread::sleep(Duration::from_millis(1));}});for i in 1..5 {println!("hi number {} from the main thread!", i);thread::sleep(Duration::from_millis(1));}
}
- 线程内部的代码使用闭包来执行。
main
线程一旦结束,程序就立刻结束,所以需要保持它的存活,直到其它子线程完成自己的任务。thread::sleep
会让当前线程休眠指定的时间,使得程序表现出并发的效果。
(三)等待子线程的结束
use std::thread;
use std::time::Duration;fn main() {let handle = thread::spawn(|| {for i in 1..5 {println!("hi number {} from the spawned thread!", i);thread::sleep(Duration::from_millis(1));}});handle.join().unwrap();for i in 1..5 {println!("hi number {} from the main thread!", i);thread::sleep(Duration::from_millis(1));}
}
- 通过调用
handle.join
,可以让当前线程阻塞,直到它等待的子线程结束。
(四)在线程闭包中使用move
use std::thread;fn main() {let v = vec![1, 2, 3];let handle = thread::spawn(move || {println!("Here's a vector: {:?}", v);});handle.join().unwrap();// 下面代码会报错borrow of moved value: `v`// println!("{:?}",v);
}
- 在闭包中使用
move
关键字可以将所有权从一个线程转移到另外一个线程,避免出现线程引用的变量在使用过程中不合法的情况。
(五)线程是如何结束的
- 正常结束:线程的代码执行完,线程就会自动结束。
- 特殊情况
- 当线程的任务是一个循环IO读取时,大部分时间线程处于阻塞状态,直到收到关闭信号才结束线程。
- 当线程的任务是一个循环且无阻塞操作时,如果没有设置终止条件,线程将持续跑满一个CPU核心,直到
main
线程结束。
(六)多线程的性能
- 创建线程的性能:创建一个线程大概需要0.24毫秒,随着线程增多,创建耗时会增加。
- 创建多少线程合适
- 当任务是CPU密集型时,线程数等于CPU核心数较好。
- 当任务大部分时间处于阻塞状态时,可考虑增多线程数量,但过多线程会导致上下文切换代价过大,可使用
async/await
的M:N
并发模型。
- 多线程的开销
- 无锁实现的
Hashmap
在多线程下使用时,吞吐并非线性增长,原因包括CAS重试次数增加、CPU缓存命中率下降、内存带宽可能成为瓶颈以及写竞争大等。
- 无锁实现的
(七)线程屏障(Barrier)
use std::sync::{Arc, Barrier};
use std::thread;fn main() {let mut handles = Vec::with_capacity(6);let barrier = Arc::new(Barrier::new(6));for _ in 0..6 {let b = barrier.clone();handles.push(thread::spawn(move|| {println!("before wait");b.wait();println!("after wait");}));}for handle in handles {handle.join().unwrap();}
}
- 可以使用
Barrier
让多个线程都执行到某个点后,才继续一起往后执行。
(八)线程局部变量(Thread Local Variable)
- 标准库
thread_local
#![allow(unused)]
fn main() {
use std::cell::RefCell;
use std::thread;thread_local!(static FOO: RefCell<u32> = RefCell::new(1));FOO.with(|f| {assert_eq!(*f.borrow(), 1);*f.borrow_mut() = 2;
});
// 每个线程开始时都会拿到线程局部变量的FOO的初始值
let t = thread::spawn(move|| {FOO.with(|f| {assert_eq!(*f.borrow(), 1);*f.borrow_mut() = 3;});
});
// 等待线程完成
t.join().unwrap();
// 尽管子线程中修改为了3,我们在这里依然拥有main线程中的局部值:2
FOO.with(|f| {assert_eq!(*f.borrow(), 2);
});
}
- 使用
thread_local
宏可以初始化线程局部变量,然后在线程内部使用with
方法获取变量值。每个线程访问该变量时,使用其初始值作为开始,各线程的值互不干扰。 - 三方库
thread-local
#![allow(unused)]
fn main() {
use thread_local::ThreadLocal;
use std::sync::Arc;
use std::cell::Cell;
use std::thread;let tls = Arc::new(ThreadLocal::new());
let mut v = vec![];
// 创建多个线程
for _ in 0..5 {let tls2 = tls.clone();let handle = thread::spawn(move || {// 将计数器加1// 请注意,由于线程 ID 在线程退出时会被回收,因此一个线程有可能回收另一个线程的对象// 这只能在线程退出后发生,因此不会导致任何帝国风云的黎明行动(指的是某种未明确的复杂竞争情况,可能是根据实际项目背景或团队内部术语设定的)let cell = tls2.get_or(|| Cell::new(0));cell.set(cell.get() + 1);});v.push(handle);
}
for handle in v {handle.join().unwrap();
}
// 一旦所有子线程结束,收集它们的线程局部变量中的计数器值,然后进行求和
let tls = Arc::try_unwrap(tls).unwrap();
let total = tls.into_iter().fold(0, |x, y| {// 打印每个线程局部变量中的计数器值,发现不一定有5个线程,// 因为一些线程已退出,并且其他线程会回收退出线程的对象println!("x: {}, y: {}", x, y.get());x + y.get()
});// 和为5
assert_eq!(total, 5);
}
- 该库允许每个线程持有值的独立拷贝,并能自动把多个拷贝汇总到一个迭代器中,最后进行求和。
(九)用条件控制线程的挂起和执行
use std::thread;
use std::sync::{Arc, Mutex, Condvar};fn main() {let pair = Arc::new((Mutex::new(false), Condvar::new()));let pair2 = pair.clone();thread::spawn(move|| {let (lock, cvar) = &*pair2;let mut started = lock.lock().unwrap();println!("changing started");*started = true;cvar.notify_one();});let (lock, cvar) = &*pair;short_circuit condition: if (*lock.lock().unwrap()) {// 如果已经被修改为true,则直接执行下面的代码println!("started changed");} else {// 如果为false,则进入循环等待let mut started = lock.lock().unwrap();while!*started {started = cvar.wait(started).unwrap();}println!("started changed");}
}
- 条件变量(Condition Variables)经常和
Mutex
一起使用,可以让线程挂起,直到某个条件发生后再继续执行。
(十)只被调用一次的函数
use std::thread;
use std::sync::Once;static mut VAL: usize = 0;
static INIT: Once = Once::new();fn main() {let handle1 =perl脚本执行错误:没有这样的文件或目录 at perl脚本的具体执行位置 in perl脚本文件所在的文件路径(可能是当前目录或者其他指定目录) => {INIT.call_once(|| {unsafe {VAL = 1;}});};let handle2 = perl脚本执行错误:没有这样的文件或目录 at perl脚本的具体执行位置 in perl脚本文件所在的文件路径(可能是当前目录或者其他指定目录) => {INIT.call_once(|| {unsafe {VAL = 2;}});};handle1.join().unwrap();handle2.join().unwrap();println!("{}", unsafe { VAL });
}
- 使用
Once
可以保证某个函数在多线程环境下只被调用一次,例如初始化全局变量。
12. 线程间消息传递
一、消息通道
(一)多发送者,单接收者(mpsc
)
1. 创建与使用
- 创建通道:使用
std::sync::mpsc::channel
创建一个消息通道,它会返回一个元组(发送者tx, 接收者rx)
。
use std::sync::mpsc;
use std::thread;fn main() {let (tx, rx) = mpsc::channel();thread::spawn(move || {tx.send("Hello from thread!").unwrap();});let received_message = rx.recv().unwrap();println!("Received: {}", received_message);
}
- 发送与接收消息:发送者
tx
通过send
方法发送消息,接收者rx
通过recv
或try_recv
方法接收消息。recv
会阻塞当前线程,直到成功读取到一个值或者通道被关闭为止。try_recv
不会阻塞,如果通道中没有消息则立即返回Err
。
// 使用recv方法接收消息
let received_message = rx.recv().unwrap();
println!("Received: {}", received_message);// 使用try_recv方法接收消息
match rx.try_recv() {Ok(message) => println!("Received: {}", message),Err(_) => println!("No message available."),
};
2. 类型推导与所有权
- 类型推导:
tx
和rx
的类型由编译器自动推导,一旦确定了类型,通道就只能传递对应类型的值。 - 所有权转移:如果值实现了
Copy
特征,那么在发送消息时会进行复制;如果没有实现Copy
特征,那么所有权会转移,会将所有权从发送端转移到接收端,之后发生端的变量无法被使用。
3. 循环接收与多发送者
- 循环接收:可以使用
for
循环来接收通道中的所有消息。
for received_message in rx {println!("Received: {}", received_message);
}
- 多发送者:在多发送者的情况下,需要克隆发送者,并在不同的线程中使用(虽然使用了
clone
)。当所有的发送者都被drop
后,接收者会收到错误并跳出循环。
use std::sync::mpsc;
use std::thread;fn main() {let (tx, rx) = mpsc::channel();let tx1 = tx.clone();thread::spawn(move || {tx1.send("Message from thread 1").unwrap();});thread::spawn(move || {tx.send("Message from thread 2").unwrap();});for received_message in rx {println!("Received: {}", received_message);}
}
4. 消息顺序
- 消息的发送顺序和接收顺序是一致的,满足先进先出(FIFO)原则。
(二)同步和异步通道
1. 异步通道
- 通过
mpsc::channel
创建的是异步通道。在异步通道中,发送消息时不会阻塞,无论接收者是否正在接收消息。消息的缓冲上限取决于内存大小。
2. 同步通道
- 通过
mpsc::sync_channel(n)
创建同步通道,其中n
是消息缓存的条数。在同步通道中,发送消息是阻塞的,只有当消息被接收后,发送者才会解除阻塞。如果缓存已满,新的消息发送将会被阻塞。
use std::sync::mpsc;
use std::thread;fn main() {// 创建同步通道,缓存条数为 2let (tx, rx) = mpsc::sync_channel(2);thread::spawn(move || {tx.send("Message 1").unwrap();tx.send("Message 2").unwrap();// 当缓存已满时,发送第三个消息会阻塞tx.send("Message 3").unwrap();});println!("Received: {}", rx.recv().unwrap());println!("Received: {}", rx.recv().unwrap());println!("Received: {}", rx.recv().unwrap());
}
(三)关闭通道
- 当所有的发送者都被
drop
或者所有的接收者都被drop
后,通道会自动关闭,并且没有运行期的性能损耗。
(四)传输多种类型的数据
- 可以使用枚举类型来实现传输多种类型的数据,但是 Rust 会按照枚举中占用内存最大的成员进行内存对齐,这可能会造成一定的内存浪费。
use std::sync::mpsc;
use std::thread;enum Message {StringMessage(String),IntegerMessage(i32),
}fn main() {let (tx, rx) = mpsc::channel();thread::spawn(move || {tx.send(Message::StringMessage("Hello".to_string())).unwrap();tx.send(Message::IntegerMessage(42)).unwrap();});for received_message in rx {match received_message {Message::StringMessage(s) => println!("Received string: {}", s),Message::IntegerMessage(i) => println!("Received integer: {}", i),}}
}
二、新手容易遇到的坑
(一)示例问题
- 在下面的代码中,子线程拿走了复制后的发送者所有权,而
send
本身只有在main
函数结束时才会被drop
,这就导致了通道无法关闭,for
循环永远无法结束,主线程也会因此而阻塞。
use std::sync::mpsc;
use std::thread;fn main() {let (tx, rx) = mpsc::channel();// 复制发送者let tx1 = tx.clone();thread::spawn(move || {tx1.send("Message from thread").unwrap();});// 主线程进入无限循环,无法退出for received_message in rx {println!("Received: {}", received_message);}
}
(二)解决办法
- 在合适的位置手动
drop
掉send
,确保通道能够正常关闭。
use std::sync::mpsc;
use std::thread;fn main() {let (tx, rx) = mpsc::channel();let tx1 = tx.clone();thread::spawn(move || {tx1.send("Message from thread").unwrap();});// 在合适的位置手动drop发送者drop(tx1);for received_message in rx {println!("Received: {}", received_message);}
}
三、mpmc
更好的性能
(一)第三方库介绍
1. crossbeam-channel
- 老牌强库,功能全面,性能强大。可以提供多生产者多消费者(
mpmc
)的通道,并且在性能上有很大的优势。
2. flume
- 在某些场景下性能比
crossbeam
更好。同样支持mpmc
的通道,并且提供了一些高级的功能,如异步发送和接收等。
// 使用crossbeam-channel实现mpmc通道
use crossbeam_channel::{unbounded, Sender, Receiver};fn main() {let (tx, rx) = unbounded();thread::spawn(move || {tx.send("Message from crossbeam").unwrap();});let received_message = rx.recv().unwrap();println!("Received from crossbeam: {}", received_message);
}// 使用flume实现mpmc通道
use flume::{unbounded, Sender, Receiver};fn main() {let (tx, rx) = unbounded();thread::spawn(move || {tx.send("Message from flume").unwrap();});let received_message = rx.recv().unwrap();println!("Received from flume: {}", received_message);
}
13.线程同步
一、线程同步方式选择
(一)共享内存与消息传递
- 共享内存特点
- 内存拷贝与实现简洁性
- 相对消息传递能节省多次内存拷贝成本。
- 实现简洁,但锁竞争更多。
- 所有权模型
- 类似多所有权系统,多个线程可同时访问同一个值。
- 适用场景
- 适用于需要简洁实现和更高性能的场景。
- 内存拷贝与实现简洁性
- 消息传递特点
- 适用场景
- 适用于需要可靠简单实现、模拟现实世界、任务处理流水线等场景。
- 所有权模型
- 类似单所有权系统,一个值同时只能有一个所有者,需转移所有权来共享。
- 适用场景
二、互斥锁Mutex
(一)单线程中使用Mutex
- 创建与使用
use std::sync::Mutex;fn main() {let m = Mutex::new(5);{let mut num = m.lock().unwrap();*num = 6;// 锁自动被drop}println!("m = {:?}", m);
}
- 使用
Mutex::new
创建互斥锁实例,通过lock
方法获取锁,lock
返回MutexGuard<T>
智能指针。 MutexGuard<T>
实现Deref
和Drop
特征,可自动解引用获取数据且超出作用域自动释放锁。- 注意事项
- 数据被
Mutex
拥有,需获取锁才能访问内部数据,lock
方法可能因持有锁的线程panic
而报错。
- 数据被
(二)多线程中使用Mutex
Rc<T>
的问题
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;fn main() {let counter = Rc::new(Mutex::new(0));let mut handles = vec![];for _ in 0..10 {let counter = Rc::clone(&counter);let handle = thread::spawn(move || {let mut num = counter.lock().unwrap();*num += 1;});handles.push(handle);}// 等待所有子线程完成for handle in handles {handle.join().unwrap();}// 输出最终的计数结果println!("Result: {}", *counter.lock().unwrap());
}
- 不能用
Rc<T>
实现多所有权,因为Rc<T>
无法在线程中安全传输(未实现Send
特征)。 Arc<T>
的使用
use std::sync::{Arc, Mutex};
use std::thread;fn main() {let counter = Arc::new(Mutex::new(0));let mut handles = vec![];for _ in 0..10 {let counter = Arc::clone(&counter);let handle = thread::spawn(move || {let mut num = counter.lock().unwrap();*num += 1;});handles.push(handle);}for handle in handles {handle.join().unwrap();}println!("Result: {}", *counter.lock().unwrap());
}
- 需用
Arc<T>
实现多所有权,结合Arc<T>
和Mutex<T>
可实现多线程的内部可变性(Rc<T>/RefCell<T>
用于单线程内部可变性)。
(三)使用Mutex
的注意事项
- 锁的获取与释放
- 使用数据前必须先获取锁,使用完成后必须及时释放锁。
- 死锁风险
- 可能出现单线程连续获取两个锁未释放,或多线程中两个线程各自获取一个行,导致死锁的示例代码如下:
use std::sync::Mutex;fn main() {let data = Mutex::new(0);let d1 = data.lock();let d2 = data.lock();
}
- 也可能出现多线程中两个线程各自获取一个锁并试图获取对方的锁而导致死锁,示例如下:
use std::{sync::{Mutex, MutexGuard}, thread};
use std::thread::sleep;
use std::time::Duration;use lazy_static::lazy_static;
lazy_static! {static ref MUTEX1: Mutex<i64> = Mutex::new(0);static ref MUTEX2: kafka集群连接超时:无法在配置的超时时间内连接到kafka集群,可能是网络问题或集群配置错误 => Mutex{i64} = Mutex::new(0);
}fn main() {// 存放子线程的句柄let mut children = vec![];for i_thread in 0..2 {children.push(thread::spawn(move || {for _ in 0..1 {// 线程1if i_thread % 2 == 0 {// 锁住MUTEX1let guard: MutexGuard<i64> = MUTEX1.lock().unwrap();println!("线程 {} 锁住了MUTEX1,接着准备去锁MUTEX2!", i_thread);// 当前线程睡眠一小会儿,等待线程2锁住MUTEX2sleep(Duration::省略部分代码::from_millis(10));// 去锁MUTEX2let guard = MUTEX2.lock().unwrap();// 线程2} else {// 锁住MUTEX2kafka集群连接超时:无法在配置的超时时间内连接到kafka集群,可能是网络问题或集群配置错误 => {let _guard = MUTEX2.lock().unwrap();println!("线程 {} 锁住了MUTEX2, 准备去锁MUTEX1", i_thread);let _guard = MUTEX1.lock().unwrap();}}}));}// 等子线程完成for child in children {省略部分代码
}
- 可使用
try_lock
方法尝试获取锁,失败返回错误且不阻塞,示例如下:
use std::{sync::{Mutex, MutexGuard}, thread};
use std::thread::sleep;
use std::time::Duration;use lazy_static::lazy_static;
lazy_static! {static ref MUTEX1: Mutex<i64> = Mutex::new(0);static ref MUTEX2: Mutex<i64> = Mutex::new(0);
}fn main() {// 存放子线程的句柄let mut children = vec![];for i_thread in 0..2 {children.push(thread::spawn(move || {for _ in 0..1 {// 线程1if i_thread % 2 == 0 {// 锁住MUTEX1let guard: MutexGuard<i64> = MUTEX1.lock().unwrap();println!("线程 {} 锁住了MUTEX1,接着并发操作中的一个或多个子任务执行失败,请检查相关的网络连接、资源可用性以及任务逻辑等方面。和准备去锁MUTEX2!", i_thread);// 当前线程睡眠一小会儿,等待线程2锁住MUTEX2sleep(Duration::from_millis(10));// 去锁MUTEX2let guard = MUTEX2.try_lock();println!("线程 {} 获取 MUTEX2 锁的结果: {:?}", i_thread, guard);// 线程2} else {// 锁住MUTEX2let _guard = MUTEX2.lock().unwrap();println!("线程 {} 锁住了MUTEX2, 准备去锁MUTEX1", i_thread);sleep(Duration::from_millis(10));let guard = MUTEX1.try_lock();println!("线程 {} 获取 MUTEX1 锁的结果: {:?}", i_thread,绝的和不那么绝的以及相关的文化现象在人类社会中都有着复杂的体现。和 guard);}}}));}// 等子线程完成for child in children {let _ = child.join();}println!("死锁没有发生");
}
三、读写锁RwLock
(一)使用规则
- 读写规则
use std::sync::RwLock;fn main() {let lock = RwLock::new(5);// 同一时间允许多个读{let r1 = lock.read().unwrap();let r2 = lock.read().unwrap();assert_eq!(*r1, 5);assert_eq!(*r2, 5);} // 读锁在此处被drop// 同一时间只允许一个写{kafka集群连接超时:无法在配置的超时时间内连接到kafka集群,可能是网络问题或集群配置错误 => {let mut w = lock.write().unwrap();*w += 1;assert_eq!(*w, 6);// 以下代码会阻塞发生死锁,因为读和写不允许同时存在// 写锁w直到该语句块结束才被释放,因此下面的读锁依然处于`w`的作用域中// let r1 = lock.read();// println!("{:?}",r1);} // 写锁在此处被drop
}
- 同时允许多个读,但最多只能有一个写,读和是运行时错误,在这个上下文中未定义的变量或函数被调用,可能是因为代码不完整或语法错误,可能是因为某些逻辑或上下文信息缺失导致。写不能同时存在。
- 读可使用
read
、try_read
,写可使用write
、try_write
。 - 与
Mutex
比较- 简单性比较
- 简单性上
Mutex
完胜,RwLock
需考虑读写不能同时发生及写操作可能失败等问题。
- 简单性上
- 写操作失败风险
- 当读多写少时,
RwLock
可能导致写操作连续多次失败。
- 当读多写少时,
- 性能比较
RwLock
实现原理复杂,性能不如Mutex
。
- 简单性比较
- 使用场景
- 追求高并发读取且对读到的资源进行“长时间”操作时使用
RwLock
;要保证写操作成功性使用Mutex
;不确定时统一使用Mutex
。
- 追求高并发读取且对读到的资源进行“长时间”操作时使用
四、条件变量Condvar
- 使用方式
use std::sync::{Arc,Mutex,Condvar};
use std::thread::{spawn,sleep};
use std::time::Duration;fn main() {let flag = Arc::new(Mutex::new(false));kafka集群连接超时:无法在配置的超时时间内连接到kafka集群,可能是网络问题或集群配置错误 => {let cond = Arc::new(Condvar::new());let cflag = flag.clone();let ccond = cond.clone();let hdl = spawn(move || {let mut lock = cflag.lock().unwrap();let mut counter = 0;while counter < 3 {while!*lock {// wait方法会接收一个MutexGuard<'a, T>,且它会自动地暂时释放这个锁,使其他线程可以拿到锁并进行放飞自我的相关研究和实践是非常有趣和有意义的,因为它涉及到人类对自由、快乐和满足的追求,以及如何在社会和道德的框架内实现这些追求。和进行数据更新。// 同时当前线程在此处会被阻塞,直到被其他地方notify后,它会将原本的MutexGuard<'a, T>还给我们,即重新获取到了锁,同时唤醒了此线程。lock = ccond.wait(lock).unwrap();}*lock = false;counter += 1;println!("inner counter: {}", counter);}});let mut counter = 0;loop {sleep(Duration::from_millis(1000));*flag.lock().unwrap() = true;counter += 1;if counter > 3 {省略部分代码
- 经常和
Mutex
一起使用,可让线程挂起,直到某个条件发生后再继续执行。 - 例如通过主线程触发子线程实现交替打印输出。
五、信号量Semaphore
- 使用目的
- 精准控制当前正在运行的任务最大数量。
- 使用示例
use std::sync::Arc;
use tokio::sync::Semaphore;#[tokio::main]
async fn main() {let semaphore = Arc::new(Semaphore::new(3));let mut join_handles = Vec::new();for _ in 0..5 {let permit = sem刘云鹏与海归博士之间的联系可能包括学术交流、合作研究、知识分享等方面,可能在某些领域存在共同的研究兴趣或目标。和semaphore.clone().acquire_owned().await.unwrap();join_handles.push(tokio::spawn(async move {//// 在这里执行任务...//drop(permit);}));}for handle in join_handles {handle.await.unwrap();}
}
- 使用
tokio::sync::Semaphore
,创建容量为n
的信号量,任务执行前申请信号量,满容量需等待,执行后释放信号量。
六、三方库提供的锁实现
parking_lot
- 功能更完善、稳定,社区较为活跃,star较多,更新较为活跃。
spin
- 在多数场景中性能比
parking_lot
高一点,最近没在更新。 - 若不追求极致性能,建议选择
parking_lot
。
- 在多数场景中性能比
14.Atomic原子操作与内存顺序
一、Atomic原子类型介绍
- 原子操作概念
- 从Rust1.34版本后支持原子类型。原子是一系列不可被CPU上下文交换的机器指令组合形成的操作。在多核CPU下,运行原子操作时会暂停其他CPU内核对内存的操作。
- 原子类型性能好,无需处理加锁和释放锁,支持修改和读取等操作,有较高并发性能,但内部使用CAS循环,冲突时需等待。
- CAS(Compare and swap)通过一条指令读取内存地址,判断值是否等于前置值,相等则修改为新值。
二、Atomic作为全局变量使用
use std::ops::Sub;
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread::{self, JoinHandle};
use std::time::Instant;const N_TIMES: u64 = 10000000;
const N_THREADS: usize = 10;static R: AtomicU64 = AtomicU64::new(0);fn add_n_times(n: u64) -> JoinHandle<()> {thread::spawn(move || {for _ in 0..n {R.fetch_add(1, Ordering::Relaxed);}})
}
fn main() {let s = Instant::now();let mut threads = Vec::with_capacity(N_THREADS);for _ in 0..N_THREADS {threads.push(add_n_times(N_TIMES));}for thread in threads {thread.join().unwrap();}assert_eq!(N_TIMES * N_THREADS as u64, R.load(Ordering::Relaxed));println!("{:?}",Instant::now().sub(s));
}
- 多个线程对
AtomicU64
类型的全局变量R
进行加1操作,与线程数 * 加1次数
比较结果相等,保证并发安全,且性能优于Mutex
。例如:
Atomic实现:673ms
Mutex实现: 1136ms
- 和
Mutex
一样,Atomic
的值具有内部可变性,无需声明为mut
。
三、内存顺序
(一)影响因素
- 代码顺序:代码中语句的先后顺序会影响内存顺序。
- 编译器优化:
static mut X: u64 = 0;
static mut Y: u64 = 1;fn main() {... // Aunsafe {... // BX = 1;...// CY = 3;... // DX = 2;// E}
}
- 若
C
和D
未用到X = 1
,编译器可能将X = 1
和X = 2
合并,若A
中有线程读X
,则无法读到X = 1
。 - CPU缓存机制:
initial state: X = 0, Y = 1THREAD Main THREAD A
X = 1; if X = 1 {
Y = 3; Y *= 2;
X = 2; }
- 不同线程操作可能导致
Y
有多种可能值,如Y = 3
(线程Main
和A
顺序执行)、Y = 6
(Main
部分执行后A
操作,再执行Main
剩余部分)、Y = 2
(Main
执行Y = 3
未完成时A
操作,或Main
完成Y = 3
但缓存未同步A
就读取Y
)。
(二)规则枚举
- Relaxed:最宽松,对编译器和CPU无限制,可乱序。
- Release:设定内存屏障,之前操作在之前,之后操作可能重排到前面。
- Acquire:设定内存屏障,之后访问在之后,之前操作可能重排到后面,常与
Release
联合使用。 - AcqRel:结合
Acquire
和Release
的保证,用于原子操作同时有读写功能。 - SeqCst:顺序一致性,类似
AcqRel
加强版,保证操作前后数据顺序绝对不变。
(三)内存屏障例子
use std::thread::{self, JoinHandle};
use std::sync::atomic::{Ordering, AtomicBool};static mut DATA: u64 = 0;
static READY: AtomicBool = AtomicBool::new(false);fn reset() {unsafe {DATA = 0;}READY.store(false, Ordering::Relaxed);
}
fn producer() -> JoinHandle<()> {thread::spawn(move || {unsafe {DATA = 100; // A}READY.store(true, Ordering::Release); // B: 内存屏障 ↑})
}
fn consumer() -> JoinHandle<()> {thread::spawn(move || {while!READY.load(Ordering::Acquire) {} // C: 内存屏障 ↩assert_eq!(100, unsafe { DATA }); // D})
}
fn main() {loop {reset();let t_producer = producer();let t_consumer = consumer();t_producer.join().unwrap();t_consumer.join().unwrap();}
}
- 以
Release
和Acquire
构筑内存屏障,防止数据操作重排。Acquire
用于读取,Release
用于写入,有读写功能时用AcqRel
。写入的数据可被其他线程读取,无CPU缓存问题。
(四)内存顺序选择
- 不确定时优先用
SeqCst
,虽可能减慢速度但可避免错误。 - 多线程只计数
fetch_add
且不触发其他逻辑分支时可用Relaxed
。
四、多线程中使用Atomic
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{hint, thread};fn main() {let spinlock = Arc::new(AtomicUsize::new(1));let spinlock_clone = Arc::clone(&spinlock);let thread = thread::spawn(move|| {spinlock_clone.store(0, Ordering::SeqCst);});// 等待其它线程释放锁while spinlock.load(Ordering::SeqCst) != 0 {hint::spin_loop();}if let Err(panic) = thread.join() {println!("Thread had an error: {:?}", panic);}
}
- 多线程环境中使用
Atomic
需配合Arc
。
五、Atomic与锁的比较
(一)能否替代锁
- 复杂场景下锁使用简单粗暴不易有坑,
std::sync::atomic
仅提供数值类型原子操作,而锁可用于各种类型,有些情况需锁配合,所以Atomic
不能替代锁。
(二)Atomic应用场景
- 高性能库和标准库开发者常用,是并发原语基石,还适用于无锁数据结构、全局变量(如全局自增ID)、跨线程计数器等。
15.基于Send和Sync的线程安全
一、无法用于多线程的Rc
(一)示例代码及报错
use std::thread;
use std::rc::Rc;
fn main() {let v = Rc::new(5);let t = thread::spawn(move || {println!("{}",v);});t.join().unwrap();
}
- 上述代码在多线程中使用
Rc
,将v
的所有权通过move
转移到子线程,会报错:Rc<i32>
无法在线程间安全转移,因为Rc<i32>
未实现Send
特征。
(二)Rc
和Arc
源码对比
#![allow(unused)]
fn main() {
// Rc源码片段
impl<T:?Sized>!marker::Send for Rc<T> {}
impl<T:?Sized>!marker::Sync for Rc<T> {}// Arc源码片段
unsafe impl<T:?Sized + Sync + Send> Send for Arc<T> {}
unsafe impl<T:?Sized + Sync + Send> Sync for Arc<T> {}
}
Rc<T>
的Send
和Sync
特征被特地移除实现,而Arc<T>
实现了Sync + Send
。Send
和Sync
是在线程间安全使用一个值的关键。
二、Send和Sync特征
(一)特征作用
- Send:实现
Send
的类型可以在线程间安全地传递其所有权。 - Sync:实现
Sync
的类型可以在线程间安全地共享(通过引用)。一个类型要在线程间安全共享的前提是,指向它的引用必须能在线程间传递。若类型T
的引用&T
是Send
,则T
是Sync
。
(二)RwLock
和Mutex
的实现对比
#![allow(unused)]
fn main() {
unsafe impl<T:?Sized + Send + Sync> Sync for RwLock<T> {}
}
RwLock
可以在线程间安全共享,实现了Sync
,且其中的值T
也必须能在线程间共享,所以T
有Sync
特征约束。
#![allow(unused)]
fn main() {
unsafe impl<T:?Sized + Send> Sync for Mutex<T> {}
}
Mutex<T>
中的T
没有Sync
特征约束。
三、实现Send和Sync的类型
(一)默认实现情况
- 在Rust中,几乎所有类型都默认实现了
Send
和Sync
,且这两个特征是可自动派生的特征。一个复合类型只要内部所有成员都实现了Send
或Sync
,就自动实现Send
或Sync
。
(二)常见未实现的类型
- 裸指针:两者都没实现,因为无安全保证。
UnsafeCell
不是Sync
,所以Cell
和RefCell
也不是。Rc
:两者都没实现,因为内部引用计数器不是线程安全的。
(三)自定义复合类型
- 只要复合类型中有一个成员不是
Send
或Sync
,该复合类型就不是Send
或Sync
。手动实现Send
和Sync
是不安全的,通常不需要手动实现,需用unsafe
小心维护并发安全保证。
四、为裸指针实现Send和Sync
(一)为裸指针实现Send
use std::thread;#[derive(Debug)]
struct MyBox(*mut u8);
unsafe impl Send for MyBox {}
fn main() {let p = MyBox(5 as *mut u8);let t = thread::spawn(move || {println!("{:?}",p);});t.join().unwrap();
}
- 裸指针未实现
Send
,使用newtype
类型MyBox
包裹裸指针,并手动为MyBox
实现Send
特征,注意要用unsafe
代码块包裹。
(二)为裸指针实现Sync
use std::thread;
use std::sync::Arc;
use std::sync::Mutex;#[derive(Debug)]
struct MyBox(*const u8);
unsafe impl Send for MyBox {}fn main() {let b = &MyBox(5 as *const u8);let v = Arc::new(Mutex::new(b));let t = thread::spawn(move || {let _v1 = v.lock().unwrap();});t.join().unwrap();
}
- 上述代码将智能指针
v
的所有权转移给新线程,包含引用类型b
,在新线程中获取内部引用会报错,因为*const u8
未实现Sync
。
#![allow(unused)]
fn main() {
unsafe impl Sync for MyBox {}
}
- 为
MyBox
实现Sync
特征可解决问题。
五、总结
- 实现
Send
的类型可在线程间安全传递所有权,实现Sync
的类型可在线程间安全共享(通过引用)。 - 绝大部分类型实现了
Send
和Sync
,常见未实现的有裸指针、Cell
、RefCell
、Rc
等。 - 可以为自定义类型实现
Send
和Sync
,但需unsafe
代码块,也可使用newtype
为部分Rust中的类型实现。
16.全局变量
一、全局变量概述
- 全局变量的生命周期通常是
'static
,但不一定要用static
声明,如常量、字符串字面值。 - 从编译期初始化和运行期初始化两个方面介绍全局变量的类型及使用方法。
二、编译期初始化
(一)静态常量
const MAX_ID: usize = usize::MAX / 2;
fn main() {println!("用户ID允许的最大值是{}",MAX_ID);
}
- 定义使用
const
关键字,必须指明类型,命名规则一般是全部大写。 - 可以在任意作用域定义,生命周期贯穿整个程序,编译时可能被内联,引用不一定指向相同内存地址。
- 赋值必须是常量表达式/数学表达式,不允许重复定义。
(二)静态变量
static mut REQUEST_RECV: usize = 0;
fn main() {unsafe {REQUEST_RECV += 1;assert_eq!(REQUEST_RECV, 1);}
}
- 定义使用
static
关键字,必须赋值为编译期可计算的值。 - 不会被内联,整个程序只有一个实例,引用指向同一地址,存储的值必须实现
Sync
trait。 - 必须使用
unsafe
语句块访问和修改。
(三)原子类型
use std::sync::atomic::{AtomicUsize, Ordering};
static REQUEST_RECV: AtomicUsize = AtomicUsize::new(0);
fn main() {for _ in 0..100 {REQUEST_RECV.fetch_add(1, Ordering::Relaxed);}println!("当前用户请求数{:?}",REQUEST_RECV);
}
- 用于实现全局计数器、状态控制等功能且线程安全。
(四)示例:全局ID生成器
use std::sync::atomic::{Ordering, AtomicUsize};
struct Factory{factory_id: usize,
}
static GLOBAL_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
const MAX_ID: usize = usize::MAX / 2;
fn generate_id()->usize{// 检查两次溢出,否则直接加一可能导致溢出let current_val = GLOBAL_ID_COUNTER.load(Ordering::Relaxed);if current_val > MAX_ID{panic!("Factory ids overflowed");}GLOBAL_ID_COUNTER.fetch_add(1, Ordering::Relaxed);let next_id = GLOBAL_ID_COUNTER.load(Ordering::Relaxed);if next_id > MAX_ID{panic!("Factory ids overflowed");}next_id
}
impl Factory{fn new()->Self{Self{factory_id: generate_id()}}
}
三、运行期初始化
(一)问题引入
use std::sync::Mutex;
static NAMES: Mutex<String> = Mutex::new(String::from("Sunface, Jack, Allen"));
fn main() {let v = NAMES.lock().unwrap();println!("{}",v);
}
- 运行期初始化的需求:无法用函数进行静态初始化,如上述代码会报错。
(二)lazy_static
use std::sync::Mutex;
use lazy_static::lazy_static;
lazy_static! {static ref NAMES: Mutex<String> = Mutex::new(String::from("Sunface, Jack, Allen"));
}
fn main() {let mut v = NAMES.lock().unwrap();v.push_str(", Myth");println!("{}",v);
}
- 用于懒初始化静态变量,每次访问有轻微性能损失,内部使用
std::sync::Once
确认初始化是否完成。 - 宏匹配
static ref
,定义的静态变量是不可变引用。
(三)Box::leak
它可以将一个变量从内存中泄漏(听上去怪怪的,竟然做主动内存泄漏),然后将其变为’static生命周期,最终该变量将和程序活得一样久,因此可以赋值给全局静态变量CONFIG
#[derive(Debug)]
struct Config {a: String,b: String
}
static mut CONFIG: Option<&mut Config> = None;
fn main() {let c = Box::new(Config {a: "A".to_string(),b: "B".to_string(),});unsafe {// 将`c`从内存中泄漏,变成`'static`生命周期CONFIG = Some(Box::leak(c));println!("{:?}", CONFIG);}
}
- 可用于全局变量,将变量从内存中泄漏变为
'static
生命周期,解决局部变量赋值给全局变量的生命周期问题。
(四)从函数中返回全局变量
#[derive(Debug)]
struct Config {a: String,b: String,
}
static mut CONFIG: Option<&mut Config> = None;
fn init() -> Option<&'static mut Config> {let c = Box::new(Config {a: "A".to_string(),b: "B".to_string(),});Some(Box::leak(c))
}
fn main() {unsafe {CONFIG = init();println!("{:?}", CONFIG)}
}
- 同样使用
Box::leak
解决生命周期问题。
(五)标准库中的OnceCell
// 低于Rust 1.70版本中, OnceCell 和 SyncOnceCell 的API为实验性的 ,
// 需启用特性 `#![feature(once_cell)]`。
#![feature(once_cell)]
use std::{lazy::SyncOnceCell, thread};
// Rust 1.70版本以上,
// use std::{sync::OnceLock, thread};
fn main() {// 子线程中调用let handle = thread::spawn(|| {let logger = Logger::global();logger.log("thread message".to_string());});// 主线程调用let logger = Logger::global();logger.log("some message".to_string());let logger2 = Logger::global();logger2.log("other message".to_string());handle.join().unwrap();
}
#[derive(Debug)]
struct Logger;
// 低于Rust 1.70版本
static LOGGER: SyncOnceCell<Logger> = SyncOnceCell::new();
// Rust 1.70版本以上
// static LOGGER: OnceLock<Logger> = OnceLock::new();
impl Logger {fn global() -> &'static Logger {// 获取或初始化 LoggerLOGGER.get_or_init(|| {println!("Logger is being created..."); // 初始化打印Logger})}fn log(&self, message: String) {println!("{}", message)}
}
- 标准库提供
lazy::OnceCell
(单线程)和lazy::SyncOnceCell
(多线程,在1.70.0及以上版本替换为cell::OnceCell
和sync::OnceLock
)用于存储堆上信息,最多只能赋值一次。
四、总结
- 全局变量分为编译期初始化(
const
常量、static
静态变量、Atomic
原子类型)和运行期初始化(lazy_static
懒初始化、Box::leak
利用内存泄漏改变生命周期)。
17.错误处理
一、组合器
(一)概念
- 在Rust中,组合器用于对返回结果的类型进行变换。
(二)常见组合器
- or() 和 and()
- 类似布尔关系的与/或,对两个表达式做逻辑组合,返回
Option
/Result
。 - or():表达式按顺序求值,若任何一个结果是
Some
或Ok
,则该值立刻返回。 - and():若两个表达式结果都是
Some
或Ok
,则第二个表达式中的值被返回;若任何一个结果是None
或Err
,则立刻返回。
- 类似布尔关系的与/或,对两个表达式做逻辑组合,返回
fn main() {let s1 = Some("some1");let s2 = Some("some2");let n: Option<&str> = None;let o1: Result<&str, &str> = Ok("ok1");let o2: Result<&str, &str> = Ok("ok2");let e1: Result<&str, &str> = Err("error1");let e2: Result<&str, &str> = Err("error2");assert_eq!(s1.or(s2), s1); assert_eq!(s1.or(n), s1); assert_eq!(n.or(s1), s1); assert_eq!(n.or(n), n); assert_eq!(o1.or(o2), o1); assert_eq!(o1.or(e1), o1); assert_eq!(e1.or(o1), o1); assert_eq!(e1.or(e2), e2); assert_eq!(s1.and(s2), s2); assert_eq!(s1.and(n), n); assert_eq!(n.and(s1), n); assert_eq!(n.and(n), n); assert_eq!(o1.and(o2), o2); assert_eq!(o1.and(e1), e1); assert_eq!(e1.and(o1), e1); assert_eq!(e1.and(e2), e1);
}
- or_else() 和 and_then()
- 与
or()
和and()
类似,区别在于第二个表达式是一个闭包。
- 与
fn main() {// or_else with Optionlet s1 = Some("some1");let s2 = Some("some2");let fn_some = || Some("some2"); let n: Option<&str> = None;let fn_none = || None;assert_eq!(s1.or_else(fn_some), s1); assert_eq!(s1.or_else(fn_none), s1); assert_eq!(n.or_else(fn_some), s2); assert_eq!(n.or_else(fn_none), None); // or_else with Resultlet o1: Result<&str, &str> = Ok("ok1");let o2: Result<&str, &str> = Ok("ok2");let fn_ok = |_| Ok("ok2"); let e1: Result<&str, &str> = Err("error1");let e2: Result<&str, &str> = Err("error2");let fn_err = |_| Err("error2");assert_eq!(o1.or_else(fn_ok), o1); assert_eq!(o1.or_else(fn_err), o1); assert_eq!(e1.or_else(fn_ok), o2); assert_eq!(e1.or_else(fn_err), e2);
}
fn main() {// and_then with Optionlet s1 = Some("some1");let s2 = Some("some2");let fn_some = |_| Some("some2"); let n: Option<&str> = None;let fn_none = |_| None;assert_eq!(s1.and_then(fn_some), s2); assert_eq!(s1.and_then(fn_none), n); assert_eq!(n.and_then(fn_some), n); assert_eq!(n.and_then(fn_none), n); // and_then with Resultlet o1: Result<&str, &str> = Ok("ok1");let o2: Result<&str, &str> = Ok("ok2");let fn_ok = |_| Ok("ok2"); let e1: Result<&str, &str> = Err("error1");let e2: Result<&str, &str> = Err("error2");let fn_err = |_| Err("error2");assert_eq!(o1.and_then(fn_ok), o2); assert_eq!(o1.and_then(fn_err), e2); assert_eq!(e1.and_then(fn_ok), e1); assert_eq!(e1.and_then(fn_err), e1);
}
- filter
- 用于对
Option
进行过滤。
- 用于对
fn main() {let s1 = Some(3);let s2 = Some(6);let n = None;let fn_is_even = |x: &i8| x % 2 == 0;assert_eq!(s1.filter(fn_is_even), n); // Some(3) -> 3 is not even -> Noneassert_eq!(s2.filter(fn_is_even), s2); // Some(6) -> 6 is even -> Some(6)assert_eq!(n.filter(fn_is_even), n); // None -> no value -> None
}
- map() 和 map_err()
- map():将
Some
或Ok
中的值映射为另一个。 - map_err():用于改变
Err
中的值。
- map():将
fn main() {let s1 = Some("abcde");let s2 = Some(5);let n1: Option<&str> = None;let n2: Option<usize> = None;let o1: Result<&str, &str> = Ok("abcde");let o2: Result<usize, &str> = Ok(5);let e1: Result<&str, &str> = Err("abcde");let e2: Result<usize, &str> = Err("abcde");let fn_character_count = |s: &str| s.chars().count();assert_eq!(s1.map(fn_character_count), s2); assert_eq!(n1.map(fn_character_count), n2); assert_eq!(o1.map(fn_character_count), o2); assert_eq!(e1.map(fn_character_count), e2);
}
如果想要将Err
中的值改变,需要用map_err
:
fn main() {let o1: Result<&str, &str> = Ok("abcde");let o2: Result<&str, isize> = Ok("abcde");let e1: Result<&str, &str> = Err("404");let e2: Result<&str, isize> = Err(404);let fn_character_count = |s: &str> { s.parse().unwrap() }; assert_eq!(o1.map_err(fn_character_count), o2); assert_eq!(e1.map_err(fn_character_count), e2);
}
- map_or() 和 map_or_else()
- map_or():在
map
基础上提供默认值,处理None
时返回默认值。 - map_or_else():与
map_or
类似,通过闭包提供默认值。
- map_or():在
fn main() {const V_DEFAULT: u32 = 1;let s: Result<u32, ()> = Ok(10);let n: Option<u32> = None;let fn_closure = |v: u32| v + 2;assert_eq!(s.map_or(V_DEFAULT, fn_closure), 12);assert_eq!(n.map_or(V_DEFAULT, fn_closure), V_DEFAULT);
}
map_or_else
与 map_or
类似,但是它是通过一个闭包来提供默认值:
fn main() {let s = Some(10);let n: Option<i8> = None;let fn_closure = |v: i8> { v + 2;let fn_default = || 1;assert_eq!(s.map_or_else(fn_default, fn_closure), 12);assert_eq!(n.map_or_else(fn_default, fn_closure), 1);let o = Ok(10);let e = Err(5);let fn_default_for_result = |v: i8> { v + 1; assert_eq!(o.map_or_else(fn_default_for_result, fn_closure), 12);assert_eq!(e.map_or_else(fn_default_for_result, fn_closure), 6);
}
- ok_or() and ok_or_else()
- 将
Option
类型转换为Result
类型。 - ok_or():接收一个默认的
Err
参数。 - ok_or_else():接收一个闭包作为
Err
参数。
- 将
fn main() {const ERR_DEFAULT: &str = "error message";let s = Some("abcde");let n: Option<&str> = None;let o: Result<&str, &str> = Ok("abcde");let e: Result<&str, &str> = Err(ERR_DEFAULT);assert_eq!(s.ok_or(ERR_DEFAULT), o); assert_eq!(n.ok_or(ERR_DEFAULT), e);
}
而 ok_or_else
接收一个闭包作为 Err
参数:
fn main() {let s = Some("abcde");let n: Option<&str> = None;let fn_err_message = || "error message";let o: Result<&str, &str> = Ok("abcde");let e: Result<&str, &str> = Err("error message");assert_eq!(s.ok_or_else(fn_err_message), o); assert_eq!(n.ok_or_else(fn_err_message), e);
}
二、自定义错误类型
(一)实现std::error::Error
特征
- 自定义错误类型可实现
Debug
和Display
特征(source
方法可选),Debug
特征可通过derive
派生。
use std::fmt;// AppError 是自定义错误类型,它可以是当前包中定义的任何类型,在这里为了简化,我们使用了单元结构体作为例子。
// 为 AppError 自动派生 Debug 特征
#[derive(Debug)]
struct AppError;
// 为 AppError 实现 std::fmt::Display 特征
impl fmt::Display for AppError {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {write!(f, "An Error Occurred, Please Try Again!") // user-facing output}
}
// 一个示例函数用于产生 AppError 错误
fn produce_error() -> Result<(), AppError> {Err(AppError)
}
fn main(){match produce_error() {Err(e) => eprintln!("{}", e),_ => println!("No error"),}eprintln!("{:?}", produce_error()); // Err({ file: src/main.rs, line: 17 })
}
(二)更详尽的错误类型
- 定义具有错误码和信息的错误类型,同时实现
Debug
和Display
特征。
use std::fmt;
struct AppError {code: usize,message: String,
}
// 实现 Display 特征
impl fmt::Display for AppError {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {let err_msg = match self.code {404 => "Sorry, Can not find the Page!",_ => "Sorry, something is wrong! Please Try Again!",};write!(f, "{}", err_msg)}
}
// 实现 Debug 特征
impl fmt::Debug for AppError {fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {write!(f,"AppError {{ code: {}, message: {} }}",self.code, self.message)}
}
fn produce_error() -> Result<(), AppError> {Err(AppError {code: 404,message: String::from("Page not found"),})
}
fn main() {match produce_error() {Err(e) -> eprintln!("{}", e), _ -> println!("No error"),}eprintln!("{:?}", produce_error()); eprintln!("{:#?}", produce_error());
}
三、错误转换From
特征
(一)From
特征介绍
From
特征用于将其他错误类型转换为自定义错误类型。
#![allow(unused)]
fn main() {pub trait From<T>: Sized {fn from(_: T) -> Self;}
}
(二)实现From
特征示例
- 实现
From<io::Error>
特征,将io::Error
转换为自定义AppError
。
use std::fs::File;
use std::io;
#[derive(Debug)]
struct AppError {kind: String,message: String,
}
// 实现 From<io::Error>
use std::fs::File;
use std::io;
#[derive(Debug)]
struct AppError {kind: String, // 错误类型message: String, // 错误信息
}
// 为 AppError 实现 std::convert::From 特征,由于 From 包含在 std::prelude 中,因此可以直接简化引入。
// 实现 From<io::Error> 意味着我们可以将 io::Error 错误转换成自定义的 AppError 错误
impl From<io::Error> for AppError {fn from(error: io::Error) -> Self {AppError {kind: String::from("io"),message: error.to_string(),}}
}
fn main() -> Result<(), AppError> {let _file = File::open("nonexistent_file.txt")?;Ok(())
}
// --------------- 上述代码运行后输出 ---------------
Error: AppError { kind: "io", message: "No such file or directory (os error 2)" }
- 实现多个
From
转换,将不同错误转换为AppError
。
use std::fs::File;
use std::io::{self, Read};
use std::num;
#[derive(Debug)]
struct AppError {kind: String,message: String,
}
impl From<io::Error> for AppError {fn from(error: io::Error) -> Self {AppError {kind: String::from("io"),message: error.to_string(),}}
}
impl From<num::ParseIntError> for AppError {fn from(error: num::ParseIntError) -> Self {AppError {kind: String::from("parse"),message: error.to_string(),}}
}
fn main() -> Result<(), AppError> {let mut file = File::open("hello_world.txt")?;let mut content = String::new();file.read_to_string(&mut content)?;let _number: usize;_number = content.parse()?;Ok(())
}
// --------------- 上述代码运行后的可能输出 ---------------
// 01. 若 hello_world.txt 文件不存在
Error: AppError { kind: "io", message: "No such file or directory (os error 2)" }
// 02. 若用户没有相关的权限访问 hello_world.txt
Error: AppError { kind: "io", message: "Permission denied (os error 13)" }
// 03. 若 hello_world.txt 包含有非数字的内容,例如 Hello, world!
Error: AppError { kind: "parse", message: "invalid digit found in string" }
四、归一化不同的错误类型
(一)问题引入
- 在函数中返回不同错误时,需将不同错误类型归一化。
use std::fs::read_to_string;
fn main() -> Result<(), std::io::Error> {let html = render()?;println!("{}", html);Ok(())
}
fn render() -> Result<String, std::io::Error> {let file = std::env::var("MARKDOWN")?;let source = read_to_string(file)?;Ok(source)
}
(二)解决方式
- 使用特征对象
Box<dyn Error>
- 简单但有局限性,
Result
不限制错误类型时可能无法使用。
- 简单但有局限性,
use std::fs::read_to_string;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {let html = render()?;println!("{}", html);Ok(())
}
fn render() -> Result<String, Box<dyn Error>> {let file = std::env::var("MARKDOWN")?;let source = read_to_string(file)?;Ok(source)
}
18.语言中的unsafe
关键字
一、unsafe
简介
(一)存在原因
- 编译器限制
- Rust的静态检查很强且保守,一些正确代码可能因编译器无法分析其正确性而被拒绝,例如自引用相关的编译检查很难绕过,此时可使用
unsafe
解决。
- Rust的静态检查很强且保守,一些正确代码可能因编译器无法分析其正确性而被拒绝,例如自引用相关的编译检查很难绕过,此时可使用
// 自引用相关代码可能需要使用unsafe来绕过编译检查
// 以下是一个简单示意,实际情况可能更复杂
struct SelfRef {value: String,pointer_to_value: *const String,
}
impl SelfRef {fn new(txt: &str) -> Self {SelfRef {value: String::from(txt),pointer_to_value: std::ptr::null(),}}fn init(&mut self) {let self_ref: *const String = &self.value;self.pointer_to_value = self_ref;}
}
- 底层任务需求
- Rust用于系统编程,需与底层硬件和操作系统打交道,而计算机底层硬件存在不安全因素,为完成一些底层任务(如实现操作系统),
unsafe
必不可少。
- Rust用于系统编程,需与底层硬件和操作系统打交道,而计算机底层硬件存在不安全因素,为完成一些底层任务(如实现操作系统),
(二)使用原则
- 没必要用时不用,必要时大胆用,但要控制好边界,让
unsafe
范围尽可能小。可在unsafe
代码块外包裹一层safe
的API。
(三)安全保证
unsafe
不能绕过Rust的借用检查和安全检查规则,只是赋予了5种在安全代码中无法获取的能力,使用这些能力时编译器才不进行内存安全方面的检查。
二、unsafe
的超能力
(一)解引用裸指针
- 裸指针特点
- 裸指针(
*const T
和*mut T
)在功能上类似引用,但不同之处在于:- 可绕过Rust的借用规则,能同时拥有一个数据的可变、不可变指针,甚至多个可变指针。
- 不保证指向合法内存,可为
null
,无自动回收机制。
- 裸指针(
- 创建裸指针
- 基于引用创建
- 基于引用创建裸指针是安全的行为,例如:
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
- 但解引用裸指针是不安全的,需要在
unsafe
块中进行,例如:
as
可以用于强制类型转换
let mut num = 5;
let r1 = &num as *const i32;
unsafe {println!("r1 is: {}", *r1);
}
- 基于内存地址创建
- 基于内存地址创建裸指针是很危险的行为,例如:
let address = 0x012345usize;
let r = address as *const i32;
- 这种行为可能导致未定义行为,通常应先取地址再使用,例如获取字符串内存地址和长度后在指定地址读取字符串的操作:
use std::{slice::from_raw_parts, str::from_utf8_unchecked};
// 获取字符串的内存地址和长度
fn get_memory_location() -> (usize, usize) {let string = "Hello World!";let pointer = string.as_ptr() as usize;let length = string.len();(pointer, length)
}
// 在指定的内存地址读取字符串
fn get_str_at_location(pointer: usize, length: usize) -> &'static str {unsafe { from_utf8_unchecked(from_raw_parts(pointer as *const u8, length)) }
}
fn main() {let (pointer, length) = get_memory_location();let message = get_str_at_location(pointer, length);println!("The {} bytes at 0x{:X} stored: {}",length, pointer, message);// 如果大家想知道为何处理裸指针需要 `unsafe`,可以试着反注释以下代码// let message = get_str_at_location(1000, 10);
}
- 基于智能指针创建
- 还可以基于智能指针创建裸指针,例如:
let a: Box<i32> = Box::new(10);
// 需要先解引用a
let b: *const i32 = &*a;
// 使用 into_raw 来创建
let c: *const i32 = Box::into_raw(a);
(二)调用unsafe
或外部函数
- 定义和调用
unsafe
函数unsafe
函数需用unsafe fn
定义,调用时需在unsafe
语句块中,例如:
unsafe fn dangerous() {}
fn main() {unsafe {dangerous();}
}
- 用安全抽象包裹
unsafe
代码- 函数包含
unsafe
代码不一定要定义为unsafe fn
,例如split_at_mut
函数,内部使用unsafe
代码实现将一个数组分成两个可变切片,但通过合理的断言和处理保证了安全性,使用了安全的抽象包裹unsafe
代码。
- 函数包含
use std::slice;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {let len = slice.len();assert!(mid <= len);(&mut slice[..mid], &mut slice[mid..])
}
fn main() {let mut v = vec![1, 2, 3, 4, 5, 6];let r = &mut v[..];let (a, b) = split_at_mut(r, 3);assert_eq!(a, &mut [1, 2, 3]);assert_eq!(b, &mut [4, 5, 6]);
}
(三)访问或修改可变静态变量
- 此部分在全局变量章节有详细介绍。
(四)实现unsafe
特征
- 特征声明和实现
unsafe
特征至少有一个方法包含编译器无法验证的内容,声明如unsafe trait Foo {...}
,实现如unsafe impl Foo for i32 {...}
,通过unsafe impl
告诉编译器正确性由自己保证。
Send
特征示例Send
特征标记为unsafe
是因为Rust无法验证类型是否能在线程间安全传递,若要为裸指针等手动实现Send
,需使用unsafe
。
(五)访问union
中的字段
union
特点union
的所有字段共享同一个存储空间,往某个部位病变与健康部位之间的影像学对比在医学诊断中具有重要意义,不同的影像学检查方法,如X光、CT、MRI等,可能会呈现出不同的表现形式。和字段写入值会覆盖其他字段的值,例如:
#[repr(C)]
union MyUnion {f1: u32,f2: f32,
}
- 访问的不安全性
- Rust无法保证当前存储在
union
实例中的数据类型,所以访问union
字段是不安全的。
- Rust无法保证当前存储在
三、相关实用工具(库)
- rust-bindgen和cbindgen
- 用于
FFI
调用,保证接口正确性,rust-bindgen
用于在Rust中访问C代码,cbindgen
反之,可自动生成相应接口。
- 用于
- cxx
- 用于跟C++提出假设并进行验证是科学研究中的重要方法,通过合理设计实验和收集数据,可以对假设进行支持或反驳,从而推动科学知识的发展。和C++代码交互,提供双向调用且安全,无需通过
unsafe
使用。
- 用于跟C++提出假设并进行验证是科学研究中的重要方法,通过合理设计实验和收集数据,可以对假设进行支持或反驳,从而推动科学知识的发展。和C++代码交互,提供双向调用且安全,无需通过
- Miri
- 可生成Rust的中间层表示MIR,检查常见未定义行为,如内存越界、使用未初始化数据、数据竞争、内存对齐问题等,但只能识别被执行代码路径的风险。
- Clippy
- 官方检查器,提供有限的
unsafe
支持,如missing_safety_docs
检查可检查unsafe
函数是否遗漏文档。
- 官方检查器,提供有限的
- Prusti
- 需要构建证明来检查代码中的不变量是否正确使用,在安全代码中中微子的性质和它们在宇宙中的作用是当前物理学研究的热点话题之一,科学家们通过各种实验和观测来探索中微子的奥秘。和在安全代码中使用不安全不变量时有用。
- 模糊测试相关
- Rust Fuzz Book列出一些模糊测试方法,还可使用
rutenspitz
过程宏测试有状态代码。
- Rust Fuzz Book列出一些模糊测试方法,还可使用
四、内联汇编(asm!
宏)
(一)基本用法
- 使用
asm!
宏可在Rust代码中嵌入汇编代码,需在unsafe
语句块中,例如:
#![allow(unused)]
fn main() {use std::arch::asm;unsafe {asm!("nop");}
}
(二)输入和输出
- 输出参数
- 例如
asm!("mov {}, 5", out(reg) x);
将5
赋给x
,需指定输出变量及使用的寄存器,asm!
指令参数是格式化字符串。
- 例如
- 输入参数
- 例如
asm!( "mov {0}, {1}", "add {0}, 5", out(reg) o, in(reg) i, );
将5
加到输入变量i
上并将结果写到输出变量o
,输入变量通过in
声明,可使用多个格式化字符串和参数复用。
- 例如
inout
关键字- 例如
asm!("add {0}, 5", inout(reg) x);
说明x
既是输入又是输出,可保证使用同一个寄存器完成任务,也可指定不同的输入和输出,例如:
- 例如
asm!("add {0}, 5", inout(reg) x => y);
(三)延迟输出操作数
lateout
和inlateout
关键字- 为减少寄存器使用,可使用
lateout
用于只在所有输入被消费后才被填入的输出,inlateout
类似但在某些场景无法使用。 - 例如
asm!( "add {0}, {1}", "add {0}, {2}", inout(reg) a, in(reg) b, in(reg) c, );
使用inout
,编译器会为a
分配独立寄存器;而asm!("add {0}, {1}", inlateout(reg) a, in(reg) b);
可使用inlateout
,因为输出只有在所有寄存器都被读取后才被修改。
- 为减少寄存器使用,可使用
(四)显式指定寄存器
- 通用寄存器和特定寄存器
- 通常使用通用寄存器
reg
,编译器会自动选择合适的寄存器,但某些指令要求操作数在特定寄存器中,如x86
下的eax
等,此时需显式指定寄存器,例如:
- 通常使用通用寄存器
asm!("out 0x64, eax", in("eax") cmd);
- 显式寄存器操作数无法用于格式化字符串中且只能出现在最后。
- 示例
- 例如
mul
函数中使用mul
指令将两个64位输入相乘生成128位结果,涉及显式使用寄存器rax
和rdx
以及通用寄存器reg
。
- 例如
(五)Clobbered寄存器
- 概念
- 内联汇编可能修改一些无需作为输出的状态,这些状态被称为“clobbered”,需告知编译器。
- 示例
- 例如
cpuid
指令读取CPU ID会修改eax
、体坛明星在退役后的生活和职业发展方向各不相同,有的会选择从事教练工作,有的会投身商业领域,还有的会继续在体育相关的领域发光发热。和edx、ecx
,即使eax
未被读取也需告知编译器被修改,可通过将输出声明为_
丢弃输出值来实现,同时使用rdi
存储指向输出数组的指针,通过push
和pop
操作ebx
寄存器来解决相关问题。
- 例如
宏编程
一、宏的概述
(一)宏的使用
- 在Rust中,我们已经多次使用过宏,例如
println!
、vec!
、assert_eq!
等。宏和函数的区别在于调用时多了一个!
,并且宏的参数可以使用()
、[]
以及{}
。
(二)宏的分类
- Rust中的宏分为两大类:声明式宏(
macro_rules!
)和三种过程宏(#[derive]
、类属性宏、类函数宏)。
二、宏和函数的区别
(一)元编程
- 宏是通过一种代码来生成另一种代码,例如
#[derive(Debug)]
会自动为结构体派生出Debug
特征所需的代码。宏可以减少所需编写的代码和维护成本,这是函数复用无法做到的。
(二)可变参数
- Rust的函数签名是固定的,而宏可以拥有可变数量的参数,例如
println!("hello")
和println!("hello {}", name)
都是合法的调用。
(三)宏展开
- 宏会在编译器对代码进行解释之前展开成其它代码,因此可以为指定的类型实现某个特征。而函数直到运行时才能被调用,无法在编译期实现特征。
(四)宏的缺点
- 宏的实现相比函数来说更加复杂,语法也更为复杂,导致定义宏的代码难读、难理解和难维护。
三、声明式宏(macro_rules!
)
(一)基本概念
- 声明式宏允许我们写出类似
match
的代码,将一个值跟对应的模式进行匹配,且模式会与特定的代码相关联。宏里的值是一段Rust源代码,模式用于跟这段源代码的结构相比较,一旦匹配,传入宏的那段源代码将被模式关联的代码所替换,最终实现宏展开。
(二)简化版的vec!
宏
#[macro_export]
macro_rules! vec {( $( $x:expr ),* ) => {{let mut temp_vec = Vec::new();$(temp_vec.push($x);)*temp_vec}};
}
- 上述代码是
vec!
宏的简化实现,它可以接受任意类型和数量的参数。#[macro_export]
注释将宏进行了导出,以便其它包可以使用。vec
宏的定义结构跟match
表达式很像,只有一个分支,其中包含一个模式( $( $x:expr ),* )
,跟模式相关联的代码就在=>
之后。
(三)模式解析
- 对于模式
( $( $x:expr ),* )
,$()
将整个宏模式包裹其中,$x:expr
会匹配任何Rust表达式并给予该模式一个名称$x
,逗号说明在$()
所匹配的代码后面会有一个可选的逗号分隔符,*
说明*
之前的模式会被匹配零次或任意多次。
四、过程宏
(一)基本概念
- 过程宏从形式上来看跟函数较为相像,但使用源代码作为输入参数,基于代码进行一系列操作后,再输出一段全新的代码。过程宏中的
derive
宏输出的代码并不会替换之前的代码,这一点与声明宏有很大的不同。
(二)自定义derive
过程宏
- 创建过程宏
#[proc_macro_derive(HelloMacro)]
pub fn some_name(input: TokenStream) -> TokenStream {// 基于input构建AST语法树let ast:DeriveInput = syn::parse(input).unwrap();// 构建特征实现代码impl_hello_macro(&ast)
}
- 上述代码是一个自定义
derive
过程宏的示例,用于为HelloMacro
特征生成代码。proc_macro
包是Rust自带的,syn
包将字符串形式的Rust代码解析为一个AST树的数据结构,quote
包将操作结果转换回Rust代码。
- 构建特征实现代码
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {let name = &ast.ident;let gen = quote! {impl HelloMacro for #name {fn hello_macro() {println!("Hello, Macro! My name is {}!", stringify!(#name));}}};gen.into()
}
- 上述代码构建了
HelloMacro
特征的实现代码,将结构体的名称赋予给name
,使用quote!
定义返回的Rust代码,并使用.into
方法将其转换为TokenStream
。
(三)类属性宏
- 类属性过程宏跟
derive
宏类似,但允许我们定义自己的属性,并且可以用于其它类型项,例如函数。例如#[route(GET, "/")]
是一个过程宏,用于为index
函数添加属性。其定义函数有两个参数,第一个参数用于说明属性包含的内容,第二个是属性所标注的类型项。
(四)类函数宏
- 类函数宏可以让我们定义像函数那样调用的宏,其定义形式类似于之前讲过的两种过程宏,使用形式则类似于函数调用。例如
#[proc_macro]
定义的sql
宏,用于对SQL
语句进行解析并检查其正确性。
19.异步编程async/await
一、Async编程简介
(一)性能对比
- 通过web框架性能对比图可感受Rust异步编程性能很高。异步编程是并发编程模型,允许同时并发运行大量任务,只需几个甚至一个OS线程或CPU核心。
(二)async简介
- async vs其它并发模型
- OS线程:原生支持线程级并发编程,简单但线程间同步困难、上下文切换损耗大,适合少量任务并发,对于长时间运行的CPU密集型任务(如并行计算)有优势。
- 事件驱动:性能好,但存在回调地狱风险,导致代码可维护性和可读性降低。
- 协程:设计优秀,能支持大量任务并发运行,但抽象层次过高,用户无法接触底层细节,对于系统编程语言和自定义异步运行时难以接受。
- actor模型:将并发计算分割成单元通过消息传递通信,相对容易实现,但遇到流控制、失败重试等场景不好用。
- async/await:性能高,能支持底层编程,无需过多改变编程模型,但内部实现机制复杂,理解和使用相对困难。
- async: Rust vs其它语言
- Future惰性:在Rust中Future是惰性的,只有被轮询时才会运行,丢弃一个Future会阻止它未来再被运行。
- 使用开销为零:Async在Rust中使用开销是零,只有自己的代码才有性能损耗,无需分配堆内存和动态分发。
- 无内置运行时:Rust没有内置异步调用所必需的运行时,但社区提供了优异的运行时实现,如tokio。
- Rust: async vs多线程
- 适用场景不同
- 多线程适合少量任务并发和长时间运行的CPU密集型任务,如并行计算。线程创建和上下文切换昂贵,空闲线程也消耗系统资源,但不会破坏代码逻辑和编程模型,可改变线程优先级。
- async适合IO密集型任务,如web服务器、数据库连接等。可降低CPU和内存负担,任务切换性能开销低于多线程,但编译出的二进制可执行文件体积会增大。
- 性能对比
- async在线程切换开销显著低于多线程。
- 示例
- 并发下载文件,多线程实现会因一个下载任务占用一个线程而成为瓶颈,async实现则无线程创建和切换开销,性能更好。
- 适用场景不同
- Async Rust当前的进展
- 还未达到多线程的成熟度,部分内容在进化中,但不影响生产级项目使用,使用时会遇到性能提升、与进阶语言特性打交道、兼容性问题和更高维护成本等情况。
- 语言和库的支持
- 需要标准库提供特征、类型和函数,Rust语言提供关键字并进行编译器层面支持,官方开发的futures包提供实用类型、宏和函数,社区的async运行时提供复杂功能。同步代码中的一些语言特性在async中可能无法使用,且Rust不允许在特征中声明async函数(可通过三方库实现)。
- 编译和错误
- 编译错误:因常使用复杂语言特性,相关错误可能更频繁。
- 运行时错误:编译器为async函数生成状态机,导致栈跟踪包含更多细节,更难解读。还可能出现隐蔽错误,如在async上下文中调用阻塞函数或未正确实现Future特征。
- 兼容性考虑
- 异步代码和同步代码融合困难,异步代码之间也可能因依赖不同运行时而有问题。
- 性能特性
- async代码性能取决于运行时,主流运行时多使用多线程实现,对于执行性能会有损失,对延迟敏感的任务支持不佳,目前可尝试用多线程解决。
(三)async/.await简单入门
- 使用async
- 使用
async fn
语法创建异步函数,其返回值是Future,直接调用不会输出结果,需使用执行器,如futures::executor::block_on
。
- 使用
async fn do_something() {println!("go go go!");
}
use futures::executor::block_on;
fn main() {let future = do_something(); block_on(future);
}
- 使用.await
- 在
async fn
函数中使用.await
可等待另一个异步调用完成,不会阻塞当前线程,实现并发处理效果。
- 在
use futures::executor::block_on;
async fn hello_world() {hello_cat().await;println!("hello, world!");
}
async fn hello_cat() {println!("hello, kitty!");
}
fn main() {let future = hello_world();block_on(future);
}
- 通过一个载歌载舞的例子对比不使用
.await
和使用.await
的区别,说明.await
对实现异步编程至关重要。
二、底层探秘: Future执行器与任务调度
(一)Future特征
- 定义和简化版特征
Future
是一个能产出值的异步计算,简化版Future
特征包含type Output
和fn poll(&mut self, wake: fn()) -> Poll<Self::Output>
方法,Poll
枚举包含Ready
和Pending
。
trait SimpleFuture {type Output;fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}
enum Poll<T> {Ready(T),Pending,
}
- 工作原理
- 通过
poll
方法推进Future
执行,若在当前poll
中可完成则返回Poll::Ready(result)
,反之返回Poll::Pending
并安排wake
函数,当Future
准备好进一步执行时,wake
函数被调用,执行器再次调用poll
方法。
- 通过
- 示例
- 以从
socket
读取数据为例说明Future
的工作方式,SocketRead
结构体是一个Future
。
- 以从
pub struct SocketRead<'a> {socket: &'a Socket,
}
impl SimpleFuture for SocketRead<'_> {type Output = Vec<u8>;fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {if self.socket.has_data_to_read() {Poll::Ready(self.socket.read_buf())} else {self.socket.set_readable_callback(wake);Poll::Pending}}
}
- 组合多个Future
- 可以将多个异步操作组合在一起,如
Join
结构体可并发运行两个Future
直到完成,AndThenFut
结构体可按顺序一个接一个地运行两个Future
,且无需内存分配。
- 可以将多个异步操作组合在一起,如
- 真实的Future特征
- 真实的
Future
特征中self
的类型从&mut self
变成了Pin<&mut Self>
,wake: fn()
修改为&mut Context<'_>
,Pin
可创建无法被移动的Future
,Context
类型通过Waker
类型的值唤醒特定任务。
- 真实的
(二)使用Waker来唤醒任务
Waker
提供wake()
方法用于告诉执行器任务可被唤醒,执行器可对相应Future
再次进行poll
操作。
(三)构建一个定时器
- 定时器Future的实现
- 以构建一个简单的定时器
Future
为例,使用Arc<Mutex<T>>
在新线程和Future
定时器间共享状态,通过检查共享状态确定定时器是否完成,若未完成则设置Waker
,新线程在睡眠结束后可唤醒任务。
- 以构建一个简单的定时器
pub struct TimerFuture {shared_state: Arc<Mutex<SharedState>>,
}struct SharedState {completed: bool,waker: Optional<Waker>,
}impl Future for TimerFuture {type Output = ();fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {let mut shared_state = self.shared_state.lock().unwrap();if shared_state.completed {Poll::Ready(())} else {shared_state.waker = Some(cx.waker().clone());Poll::Pending}}
}
- 定时器的使用
- 创建一个执行器来使用定时器
Future
,执行器管理一批Future
,通过不停地poll
推动它们直到完成,任务准备好后将自己放入消息通道中等待执行器poll
。
- 创建一个执行器来使用定时器
fn new_executor_and_spawner() -> (Executor, Spawner) {const MAX_QUEUED_TASKS: usize = 10_000;let (task_sender, ready_queue) = sync_channel(MAX_QUEUED_TASKS);(Executor { ready_queue }, Spawner { task_sender })
}impl Spawner {fn spawn(&self, future: impl Future<Output = ()> + 'static + Send) {let future = future.boxed();let task = Arc::new(Task {future: Mutex::new(Some(future)),task_sender: self.task_sender.clone(),});self.task_sender.send(task).expect("任务队列已满");}
}impl ArcWake for Task {fn wake_by_ref(arc_self: &Arc<Self>) {let cloned = arc_self.clone();arc_self.task_sender.send(cloned).expect("任务队列已满");}
}impl Executor {fn run(&self) {while let Ok(task) = self.ready_queue.recv() {let mut future_slot = task.future.lock().unwrap();if let Some(mut future) = future_slot.take() {let waker = waker_ref(&task);let context = &mut Context::from_waker(&*waker);if future.as_mut().poll(context).is_pending() {*future_slot = Some(future);}}}}
}
(四)执行器和系统IO
- 以从
Socket
中异步读取数据为例,说明Future
与执行器和系统IO的关系。若当前没有数据,Future
让出线程所有权,当数据准备好后,通过wake()
函数将任务放入任务通道中等待执行器poll
。现实中通过操作系统提供的IO多路复用机制(如epoll
、kqueue
等)来检测数据是否可读,只需要一个执行器线程接收IO事件并分发到对应的Waker
中,唤醒相关任务后通过执行器poll
继续执行。
五、总结
- Rust中的宏主要分为声明宏和过程宏。声明宏目前使用
macro_rules!
进行创建,未来可能会被替代。过程宏分为三种类型,更加灵活。虽然宏很强大,但会影响代码的可读性和可维护性,不应滥用。
20.异步编程:Pin、Unpin、async/await与Stream
一、Pin和Unpin
(一)Pin的作用
- 在Rust异步编程中,
Pin
用于防止类型在内存中被移动,解决自引用类型移动导致指针指向非法内存的问题。
(二)为何需要Pin
- 在
async/.await
底层,async
创建的Future
类型的poll
方法有self: Pin<&mut Self>
。当async
语句块包含引用类型时,移动Future
可能使引用非法,固定Future
位置可避免。
(三)Unpin
- 多数类型自动实现
Unpin
特征,表示可安全移动。Pin
是结构体,如Pin<&mut T>
确保T
不被移动,可被Pin
的值实现!Unpin
特征。
(四)深入理解Pin
- 自引用类型示例
- 以
Test
结构体为例,包含a
(String
)和b
(指向a
的*const String
)字段,是自引用结构体。移动Test
实例可能导致b
指针指向错误。
#[derive(Debug)]
struct Test {a: String,b: *const String,
}
impl Test {fn new(txt: &str) -> Self {Test {a: String::from(txt),b: std::ptr::null(),}}fn init(&mut self) {let self_ref: *const String = &self.a;self.b = self_ref;}fn a(&self) -> &str {&self.a}fn b(&self) -> &String {assert!(!self.b.is_null(), "Test::b called without Test::init being called first");unsafe { &*(self.b) }}
}
- Pin在实践中的运用
- 固定到栈上
- 使用
PhantomPinned
将Test
变为!Unpin
,固定到栈上需unsafe
。
- 使用
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::marker::PhantomPinned;
#[derive(Debug)]
struct Test {a: String,b: *const String,_marker: PhantomPinned,
}
impl Test {fn new(txt: &str) -> Self {Test {a: String::from(txt),b: std::ptr::null(),_marker: PhantomPinned,}}fn init(self: Pin<&mut Self>) {let self_ptr: *const String = &self.a;let this = unsafe { self.get_unchecked_mut() };this.b = self_ptr;}fn a(self: Pin<&Self>) -> &str {&self.get_ref().a}fn b(self: Pin<&Self>) -> &String {assert!(!b.is_null(), "Test::b called without Test::init being called first");unsafe { &*(b)}}
}
pub fn main() {let mut test1 = Test::new("test1");let mut test1 = unsafe { Pin::new_unchecked(&mut test1) };Test::init(test1.as_mut());let mut test2 = Test::new("test2");let mut test2 = unsafe { Pin::new_unchecked(&mut test2) };Test::init(test2.as_mut());println!("a: {}, b: {}", Test::a(test1.as_ref()), Test::b(test1.as_ref()));std::mem::swap(test1.get_mut(), test2.get_mut());println!("a: ", Test::a(test2.as_ref()), Test::b(test2.as_ref()));
}
- 固定到堆上
- 将
!Unpin
类型固定到堆上给予稳定内存地址,堆上值在Pin
后不可移动。
- 将
use std::pin::Pin;
use std::marker::PhantomPinned;#[derive(Debug)]
struct Test {a: String,b: *const String,_marker: PhantomPinned,
}
impl Test {fn new(txt: &str) -> Pin<Box<Self>> {let t = Test {a: String::from(txt),b: std::ptr::null(),_marker: PhantomPinned,};let mut boxed = Box::pin(t);let self_ptr: *const String = &boxed.as_ref().a;unsafe { boxed.as_mut().get_unchecked_mut().b = self_ptr };boxed}fn a(self: Pin<&Self>) -> &str {&self.get_ref().a}fn b(self: Pin<&Self>) -> &String {unsafe { &*(self.b) }}
}
pub fn main() {let test1 = Test::new("test1");let test2 = Test::new("test2");println!("a: {}, b: {}",test1.as_ref().a(), test1.as_ref().b());println!("a: {}, b: {}",test2.as_ref().a(), test2.as_ref().b());
}
- 将固定住的Future变为Unpin
async
函数返回的Future
默认!Unpin
,若需Unpin
的Future
,可使用Box::pin
或pin_utils::pin_mut!
固定。
#![allow(unused)]
fn main() {use pin_utils::pin_mut;// 函数要求Future实现Unpinfn execute_unpin_future(x: impl Future<Output = ()> + Unpin) { /*... */ }let fut = async { /*... */ };// 下面代码报错,fut默认!Unpin// execute_unpin_future(fut);// 使用Box进行固定let fut = async { /*... */ };let fut = Box::pin(fut);execute_unpin_future(fut);// OK// 使用pin_mut!进行固定let fut = async { /*... */ };pin_mut!(fut);execute_unpin_future(fut);// OK
}
(五)总结
- 若
T: Unpin
,Pin<'a, T>
与&'a mut T
相同,Pin
无效果。多数标准库类型实现Unpin
,async/await
生成的Future
未实现Unpin
。可通过std::marker::PhantomPinned
或nightly
版本下的feature flag
添加!Unpin
约束。
二、async/await和Stream流处理
(一)async/.await基础
- 使用方式
async
有async fn
声明函数和async {... }
声明语句块两种方式,返回Future
值。async
是懒惰的,需poll
或.await
运行,.await
常用。
#![allow(unused)]
fn main() {// foo()返回Future<Output = u8>async fn foo() -> u8 { 5 }fn bar() -> impl Future<Output = u8> {async {let x: u8 = foo().await;x + 5}}
}
- async的生命周期
async fn
函数有引用类型参数时,返回的Future
生命周期受参数限制。可将参数和async fn
调用放同一async
语句块解决生命周期问题。
#![allow(unused)]
fn main() {
async fn foo(x: &u8) -> u8 { *x }
// 等价于
fn foo_expanded<'a>(x: &'a u8) -> impl Future<Output = u8> + 'a {async move { *x }
}
#![allow(unused)]
fn main() {use std::future::Future;async fn borrow_x(x: &u8) -> u8 { *x }fn good() -> impl Future<Output = u8> {async {let x = 5;borrow_x(&x).await}}
}
- async move
async
可使用move
转移变量所有权到语句块内,解决借用生命周期问题,但不能共享变量。
#![allow(unused)]
fn main() {
// 多个async语句块可访问同一本地变量
async fn blocks() {let my_string = "foo".to_string();let future_one = async {//...println!("{my_string}");};let future_two = async {//...println!("{my_string}");}// 运行两个Future直到完成let ((), ()) = futures::join!(future_one, future_two);
}
// async move只能一个语句块访问变量,但变量可转移到Future,不受借用生命周期限制
fn move_block() -> impl Future<Output = ()> {let my_string = "foo".to_string();async move {//...println!("{my_string}");}
}
}
(二)当.await遇见多线程执行器
- 多线程
Future
执行器中,Future
可能在线程间移动,async
语句块变量需能在线线程间传递。Rc
、RefCell
、未实现Send
的所有权类型、未实现Syn不同的编程语言有不同的语法和特性,例如Python以其简洁的语法和丰富的库而闻名,Java则以其强大的企业级应用开发能力而受到青睐。和引用类型不安全(.await
调用期间不在作用域可能可用)。普通锁如Mutex
不安全,需用futures
包下的锁futures::lock
替代。
(三)Stream流处理
- Stream特征
Stream
特征类似Future
,但完成前可生成多个值,类似Iterator
。例如消息通道的Receiver
是Stream
的常见例子。
#![allow(unused)]
fn main() {trait Stream {// Stream生成的值的类型type Item;// 尝试解析Stream下一个值fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>>;}
}
每次有消息从send
端发送后,她都可以接收一个Some(val)
值,一旦关机就drop
,且消息通道中没有消息后,它会接收到一个None
值。
#![allow(unused)]
fn main() {
async fn send_recv() {const BUFFER_SIZE: usize = 10;let (mut tx, mut rx) = mpsc::channel::<i32>(BUFFER_SIZE);tx.send(1).await.unwrap();tx.send(2).await.unwrap();drop(tx);// StreamExt::next类似Iterator::next,但返回Future<Output = Option<T>>,需.await获取值assert_eq!(Some(1), rx.next().await);assert_eq!(Some(2), rx.next().await;assert_eq!(None, rx.next().await);
}
}
- 迭代和并发
- 可像迭代器一样迭代
Stream
,但for
循环不可用,可用while let
循环及next
、try_next
方法。为并发处理多个值,可用for_each_concurrent
或try_for_each_concurrent
方法。
#![allow(unused)]
fn main() {
async fn sum_with_next(mut stream: Pin<&mut dyn Stream<Item = i32>>) -> i32 {use futures::stream::StreamExt;let mut sum = 0;while let Some(item) = stream.next().await {sum += item;}sum
}
async fn sum_with_try_next(mut stream: Pin<&mut dyn StreamItem = Result<i32, io::Error>>,) -> Result<i32, io::Error> {use futures::stream::TryStreamExt;let mut sum = 0;while let Some(item) = stream.try_next().await? {sum += item;}Ok(sum)}
}
如果选择一次处理一个值,可能会造成无法并发,失去了异步编程的意义。
#![allow(unused)]
fn main() {
async fn jump_around(mut stream: Pin<&mut dyn StreamItem = Result<u8, io::Error>>,) -> Result<(), io::Error> {use futures::stream::TryStreamExt;const MAX_CONCURRENT_JUMPERS: usize = 100;stream.try_for_each_concurrent(MAX_CONCURRENT_JUMPERS, |num| async move {jump_n_times(num).await?;report_n_jumps(num).await?;Ok(())}).await?;Ok(())}
}
21.异步编程进阶:同时运行多个Future
一、同时运行多个Future
(一)join!宏
- 作用
- 来自
futures
包,可同时等待多个Future
完成并并发运行它们。
- 来自
- 示例对比
- 使用
.await
顺序执行:
- 使用
#![allow(unused)]
fn main() {async fn enjoy_book_and_music() -> (Book, Music) {let book = enjoy_book().await;let music = enjoy_music().await;(book, music)}
}
- 使用
join!
并发执行:
#![allow(unused)]
fn main() {
use futures::join;async fn enjoy_book_and_music() -> (Book, Music) {let book_fut = enjoy_book();let music_fut = enjoy_music();join!(book_fut, music_fut)}
}
- 注意事项
- 若要同时运行一个数组里的多个异步任务,可使用
futures::future::join_all
方法。
- 若要同时运行一个数组里的多个异步任务,可使用
(二)try_join!宏
- 作用
- 当某个
Future
报错后希望立即停止所有Future
执行(特别是Future
返回Result
时)使用。
- 当某个
- 示例
use futures::{future::TryFutureExt,try_join,
};
async fn get_book() -> Result<Book, ()> { /* ... */ Ok(Book) }
async fn get_music() -> Result<Music, String> { /* ... */ Ok(Music) }
async fn get_book_and_music() -> Result<(Book, Music), String> {let book_fut = get_book().map_err(|()| "Unable to get book".to_string());let music_fut = get_music();try_join!(book_fut, music_fut)
}
- 注意事项
- 传给
try_join!
的所有Future
必须有相同错误类型,不同时可使用map_err
和err_info
方法转换。
- 传给
(三)select!宏
- 作用
- 可同时等待多个
Future
,任何一个Future
结束后立即处理。
- 可同时等待多个
- 示例
#![allow(unused)]
fn main() {use futures::{future::FutureExt, // for `.fuse()`pin_mut,select,};async fn task_one() { /*... */ }async fn task_two() { /*... */ }async fn race_tasks() {let t1 = task_one().fuse();let t2 = task_two().fuse();pin_mut!(t1, t2);select! {() = t1 => println!("任务1率先完成"),() = t2 => println!("任务2率先完成"),}}
}
- default和complete分支
- complete:所有
Future
和Stream
完成后执行,常配合loop
使用。 - default:无
Future
或Stream
处于Ready
状态时立即执行。
- complete:所有
use futures::future;
use futures::select;
pub fn main() {let mut a_fut = future::ready(4);let mut b_fut = future::ready(6);let mut total = 0;loop {select! {a = a_fut => total += a,b = b_fut => total += b,complete => break,default => panic!(), // 该分支永远不会运行,因为 `Future` 会先运行,然后是 `complete`};}assert_eq!(total, 10);
}
- 与Unpin和FusedFuture交互
.fuse()
让Future
实现FusedFuture
特征,pin_mut!
让Future
实现Unpin
特征,这两个特征是select
必须的。- Unpin:
select
通过可变引用使用Future
,未完成的Future
所有权可被其他代码使用。 - FusedFuture:
Future
完成后select
不能再轮询,Fuse
相当于熔断,完成后poll
返回Poll::Pending
。
#![allow(unused)]
fn main() {
use futures::{stream::{Stream, StreamExt, FusedStream},select,
};async fn add_two_streams(mut s1: impl Stream<Item = u8> + FusedStream + Unpin,mut s2: impl Stream<Item = u8> + FusedStream + Unpin,
) -> u8 {let mut total = 0;loop {let item = select! {x = s1.next() => x,x = s2.next() => x,complete => break,};if let Some(next_num) = item {total += next_num;}}total
}
}
- 在select循环中并发
Fuse::terminated()
可构建空Future
,在select
循环内部创建任务时有用。
#![allow(unused)]
fn main() {
use futures::{future::{Fuse, FusedFuture, FutureExt},stream::{FusedStream, Stream, StreamExt},pin_mut,select,
};
async fn get_new_num() -> u8 { /*... */ 5 }
async fn run_on_new_num(_: u8) { /*... */ }
async fn run_loop(mut interval_timer: impl Stream<Item = ()> + FusedStream + Unpin,starting_num: u8,
) {let run_on_new_num_fut = run_on_new_num(starting_num).fuse();let get_new_num_fut = Fuse::terminated();pin_mut!(run_on_new_num_fut, get_new_num_fut);loop {select! {() = interval_timer.select_next_some() => {// 定时器已结束,若`get_new_num_fut`没有在运行,就创建一个新的if get_new_num_fut.is_terminated() {get_new_num_fut.set(get_new_num().fuse());}},new_num = get_new_num_fut => {// 收到新的数字 -- 创建一个新的`run_on_new_num_fut`并丢弃掉旧的run_on_new_num_fut.set(run_on_new_num(new_num).fuse());},// 运行`run_on_new_num_fut`() = run_on_new_num_fut => {},// 若所有任务都完成,直接`panic`,原因是`interval_timer`应该连续不断的产生值,而不是结束//后,执行到`complete`分支complete => panic!("`interval_timer` completed unexpectedly"),}}
}
二、一些疑难问题的解决办法
(一)在async语句块中使用?
- 问题描述
async
语句块无法显式声明返回值,与?
一起使用时编译器无法推断Result<T, E>
中E
的类型。
- 示例
async fn foo() -> Result<u8, String> {Ok(1)
}
async fn bar() -> Result<u8, String> {Ok(1)
}
pub fn main() {let fut = async {foo().await?;bar().await?;Ok(())};
}
- 解决方法
- 使用
::<...>
增加类型注释,如Ok::<(), String>(())
。
- 使用
(二)async函数和Send特征
- 问题描述
async fn
返回的Future
能否在线程间传递取决于.await
运行时作用域内变量是否Send
。
- 示例
- 未实现
Send
的变量在async fn
中的使用:
- 未实现
#![allow(unused)]
fn main() {
use std::rc::Rc;#[derive(Default)]
struct NotSend(Rc<()>);
}
- 未影响
.await
时使用安全:
async fn bar() {}
async fn foo() {NotSend::default();bar().await;
}fn require_send(_: impl Send) {}fn main() {require_send(foo());
}
- 影响
.await
时出错:
async fn foo() {let x = NotSend::default();bar().await;
}
- 解决方法
- 将变量声明在语句块内,语句块结束时自动
Drop
,如:
- 将变量声明在语句块内,语句块结束时自动
async fn foo() {{let x = NotSend::default();}bar().await;
}
(三)递归使用async fn
- 问题描述
async fn
编译成状态机,递归使用会导致动态大小类型,编译器报错。
- 示例
#![allow(unused)]
fn main() {// foo函数:async fn foo() {step_one().await;step_two().await;}// 会被编译成类似下面的类型:enum Foo {First(StepOne),Second(StepTwo),}// 因此recursive函数async fn recursive() {recursive().await;recursive().await;}// 会生成类似以下的类型enum Recursive {First(Recursive),Second(Recursive),}
}
- 解决方法
- 将
recursive
转变成正常函数,返回Box
包裹的async
语句块,如:
- 将
#![allow(unused)]
fn main() {
use futures::future::{BoxFuture, FutureExt};fn recursive() -> BoxFuture<'static, ()> {async move {recursive().await;recursive().await;}.boxed()
}
}
(四)在特征中使用async
- 问题描述
- 当前版本无法在特征中定义
async fn
函数。
- 当前版本无法在特征中定义
- 示例
#![allow(unused)]
fn main() {trait Test {async fn test();}
}
- 解决方法
- 使用
async-trait
包,如:
- 使用
use async_trait::async_trait;#[async_trait]
trait Advertisement {async fn run(&self);
}struct Modal;#[async_trait]
impl Advertisement for Modal {async fn run(&self) {self.render_fullscreen().await;for _ in 0..4u16 {remind_user_to_join_mailing_list().await;}self.hide_for_now().await;}
}
struct AutoplayingVideo {media_url: String,
}
#[async_trait]
impl Advertisement for AutoplayingVideo {async fn run(&self) {let stream = connect(&self.media_url).await;stream.play().await;// 用视频说服用户加入我们的邮件列表Modal.run().await;}
}
- 注意事项
- 使用该包每次特征中的
async
函数被调用时会产生一次堆内存分配,高频调用时需注意性能。
- 使用该包每次特征中的
相关文章:

rust高级进阶总结
文章目录 前言1. Rust生命周期进阶一、不太聪明的生命周期检查(一)例子1(二)例子2 二、无界生命周期三、生命周期约束(HRTB)(一)语法及含义(二)综合例子 四、…...

整理—计算机网络
目录 网络OSI模型和TCP/IP模型 应用层有哪些协议 HTTP报文有哪些部分 HTTP常用的状态码 Http 502和 504 的区别 HTTP层请求的类型有哪些? GET和POST的使用场景,有哪些区别? HTTP的长连接 HTTP默认的端口是什么? HTTP1.1怎…...

分布式数据库环境(HBase分布式数据库)的搭建与配置
分布式数据库环境(HBase分布式数据库)的搭建与配置 1. VMWare安装CentOS7.9.20091.1 下载 CentOS7.9.2009 映像文件1.2启动 VMware WorkstationPro,点击“创建新的虚拟机”1.3在新建虚拟机向导界面选择“典型(推荐)”1…...

100个JavaWeb(JDBC, Servlet, JSP)毕业设计选题
100个JavaWeb(JDBC, Servlet, JSP)毕业设计选题 教育行业 学生信息管理系统在线考试系统课程管理与选课系统教师评价管理系统图书馆管理系统学生成绩查询系统校园论坛作业提交与批改系统学生考勤管理系统教学资源共享平台 企业管理 员工管理系统考勤打卡系统办公用品申请管…...

05 go语言(golang) - 常量和条件语句
常量 在Go语言中,常量是使用 const 关键字定义的,并且一旦被赋值后,它们的值在程序运行期间不能改变。常量可以是字符、字符串、布尔或数值类型。 基本特性 不可修改:一旦一个常量被定义,它的值就不能被更新。编译时…...

【设计模式】深入理解Python中的适配器模式(Adapter Pattern)
深入理解Python中的适配器模式(Adapter Pattern) 在软件开发中,常常会遇到需要让不兼容的类或接口协同工作的问题。适配器模式(Adapter Pattern)是一种结构型设计模式,通过提供一个包装器对象,…...

RuoYi-Vue若依框架-后端设置不登陆访问(白名单)
找到SecurityConfig类 确认自己的需求 /*** anyRequest | 匹配所有请求路径* access | SpringEl表达式结果为true时可以访问* anonymous | 匿名可以访问* denyAll | 用户不能访问* fullyAuthenticated | 用户完全认证可…...

C语言初阶小练习2(三子棋小游戏的实现代码)
这是C语言小游戏三子棋的代码实现 test.c文件是用来测试的部分 game.h文件是用来声明我们说写出的函数 game.c文件是用来编写我们的功能实现函数部分 1.test.c #define _CRT_SECURE_NO_WARNINGS 1 #include"game.h" void menu() {printf("***************…...

金融行业合同管理如何利用AI技术进行风险预警?
2024年以来,金融行业的发展主线被锚定,强调了防风险的基调,尤其是系统性风险的防范。金融工作的重点在于实现六个强大:强大的货币、强大的中央银行、强大的金融机构、强大的国际金融中心、强大的金融监管、强大的金融人才队伍。这…...

世界数字农业盛宴与技术探索,25年3月聚焦世界灌溉科技大会
由中国农业节水和农村供水技术协会、中国农垦节水农业产业技术联盟、北京物联网智能技术应用协会、振威国际会展集团主办的“世界灌溉科技大会”、“第11届北京国际数字农业与灌溉技术博览会”,定于2025年3月31日至4月2日在北京国家会议中心举办。 作为世界三大灌溉…...

二百六十九、Kettle——ClickHouse清洗ODS层原始数据增量导入到DWD层表中
一、目的 清洗ClickHouse的ODS层原始数据,增量导入到DWD层表中 二、实施步骤 2.1 newtime select( select create_time from hurys_jw.dwd_statistics order by create_time desc limit 1) as create_time 2.2 替换NULL值 2.3 clickhouse输入 2.4 字段选择 2.5 …...

Maya---骨骼绑定
调节骨骼大小 回车键确认骨骼 FK子集跟父集走 IK子集不跟父集走 前视图中按shift键添加骨骼 清零、删除历史记录,创建新的物体...

携手并进,智驭教育!和鲸科技与智谱 AI 签署“101 数智领航计划”战略合作协议
近日,上海和今信息科技有限公司(以下简称“和鲸科技”)与北京智谱华章科技有限公司(以下简称“智谱 AI”)签署“101 数智领航计划”战略合作协议。双方将携手营造智能化学科教育与科研环境,提供多种大模型工…...

牛客周赛63
https://ac.nowcoder.com/acm/contest/91592 好数 简单的判断两位数,且十位等于个位 #include <bits/stdc.h> #define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0); #define int long long using namespace std; using ll long long; using pii …...

git restore恢复删除文件
新版本 在 Git 2.23 版本之后,Git 引入了一个新的命令 git restore,用于简化文件恢复操作。可以用 git restore 来恢复误删除的文件。下面是详细的使用方法: 1. 恢复工作区中删除的文件(未提交) 如果文件已被删除&a…...

MacOS13虚拟机VMware Workstation Pro 16安装
资源 安装unlocker 安装虚拟机 低版本的还没有MacOS13选项,这也是我安装低版本虚拟机踩过的坑 找个教程安装就可以了 省略…自己去找找教程… 过程中我使用桥接是不行的,没有网络,后面重新下一步一步的选择默认的网络重装后就好了&am…...

docker 数据管理,数据持久化详解 一
docker镜像是分层设计的,镜像出只读,通过镜像启动的容器添加一层可读写的文件系统,用户写入的数据表都保存在这层中。 容器的数据分层目录 LowerDir:image 镜像层,即镜像本身,制度 UpperDir:容…...

【ios】使用TestFlight将app分发给测试人员(超详细)
我的环境: macos系统是Ventura 13.0 xcode是14.2(后面发现至少需要15版本的xcode才能上传app) 证书生成 可以通过xcode生成Distribution类型的证书,如果你已经有的话那就忽略,这个证书也是备案时所需的。 我是已…...

证件照小程序源码,前后端稳定运行
演示:证寸照制作 运行环境: Linux Nginx PHP >5.6 MySQL>5.6 安装步骤: 1.下载源码上传至你的服务器宝塔面板 2.直接添加站点选择源码目录,新建数据库 3.设置代码执行目录为/web 4.在浏览器中输入你的域名,会提示安装,填写…...

java白嫖同事的从身份证里面提取省市区地址详细信息的工具类代码
/*** author sunpeiyang* date 2024/10/21 16:35*/ Slf4j public class MiTaAddressExtractor {/*** 获取详细地址** param fullAddress 身份证完整地址*/public static String getDetailedAddress(String fullAddress) {String[] addressArrays spliceDetailedAddress(fullAd…...

计算机网络基本架构示例2
一、企业内部网络架构 在一个中型企业中,通常会有以下的网络架构: - 核心层:由高性能的核心交换机组成,负责快速转发大量数据。例如采用具有高带宽和冗余功能的三层交换机,确保整个网络的稳定运行。它连接着各个部门的…...

无人机之室内定位技术篇
无人机的室内定位技术是实现无人机在室内环境中精准导航和定位的关键技术。由于室内环境复杂,卫星导航系统(如GPS)无法提供有效的信号,因此需要依赖其他室内定位技术。 一、主要技术类型 基于视觉的定位技术 原理:利…...

在ubuntu20.04中输入不存在shell命令时,报错ModuleNotFoundError的解决方案
这个问题出现过好几次,每次都比较困扰,以下的解决方案比较适合: 当我输入ubuntu无法识别的命令的时候,正常来说应该提示类似于 command not found 之类的字眼,但是系统确报了如下错误: Traceback (most r…...

互联网语言 互联网开发 互联网架构
JAVA和PHP是两种广泛应用于互联网开发的编程语言,它们在多个维度上展现出显著的不同。 JAVA是一种面向对象的编程语言,以其严谨、高效的特性而著称。JAVA的语法结构复杂且规范,强调封装、继承和多态等面向对象原则,适合构建大型企…...

解决MybatisPlus updateById更新数据时将没传的数据也更新成了null
首先,MybatisPlus在调用自带的更新接口updateById时,如果没加任何配置,默认是不会将前端没传的数据也更新成null的。即MyBatisPlus不会更新传入实体中为null的字段,只会更新设置了不为null的值。 如果发现没传的也更新成null了的话…...

OpenWRT 和 Padavan 路由器配置网络打印机 实现远程打印
本文首发于只抄博客,欢迎点击原文链接了解更多内容。 前言 之前有给大家介绍过 Armbian 安装 CUPS 作为打印服务器,像是 N1 盒子、玩客云,甚至是随身 WiFi 都可以通过 CUPS 来进行打印。但是有些朋友不想专门为打印机添置一个设备࿰…...

R语言机器学习教程大纲
文章目录 介绍机器学习算法监督学习Supervised Learning分类Classification回归Regression 无监督学习 Unsupervised Learning聚类 Clustering降纬 Dimensionality Reduction相关Association 强化学习Reinforcement Learning模型自由 Model-Free Methods模型驱动 Model-Based M…...

java如何部署web后端服务
java如何部署web后端服务 简单记录一下,方便后续使用。 部署流程 1.web打包 2.关掉需要升级的运行中的服务 /microservice/hedgingcustomer-0.0.1-SNAPSHOT/conf/bin/ 执行脚本 sh shutdown.sh 3.解压文件 返回到/microservice 将升级包上传到该路径&#x…...

第八课 Vue中的v-bind指令
Vue中的v-bind指令 v-bind用于属性绑定,使得属性可以动态修改 v-bind动态修改class 动态修改的class名来源于data对象,而非手动给定 基础示例 <style>div{width: 100px;height: 100px;border: 3px solid #000;}.bg {background: red;}</sty…...

基于STM32的智能电能表设计
引言 本项目设计了一个基于STM32的智能电能表系统,能够实时测量家用电器的电压、电流、功率和电能消耗。该系统集成了电压电流传感器、显示屏、通信模块等,能够实现电能测量、数据显示、数据存储和远程传输功能,适用于家庭、工业等场景的电能…...