网站开发专业的建设设想,施工员证书查询网站,微信群推广平台,诺诚建设工程有限公司网站文章目录 前言1. Rust生命周期进阶一、不太聪明的生命周期检查#xff08;一#xff09;例子1#xff08;二#xff09;例子2 二、无界生命周期三、生命周期约束#xff08;HRTB#xff09;#xff08;一#xff09;语法及含义#xff08;二#xff09;综合例子 四、… 文章目录 前言1. Rust生命周期进阶一、不太聪明的生命周期检查一例子1二例子2 二、无界生命周期三、生命周期约束HRTB一语法及含义二综合例子 四、闭包函数的消除规则一问题示例二原因分析三解决方法 五、NLLNon - Lexical Lifetime一规则变化二示例及分析 六、Reborrow再借用一概念及示例二错误示例 七、生命周期消除规则补充一impl块消除二生命周期约束消除 八、复杂例子分析一代码及错误二原因分析三解决方法 2. static和T: static一、static的常见情况一字符串字面量二特征对象 二、static一含义二示例及注意事项 三、T: static一约束情况二示例及分析1. 函数中直接使用T2. 函数中使用T3. 另一个示例 四、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.BoxT堆对象分配一、Rust中的堆栈一堆栈概念二堆栈性能 二、BoxT的使用场景一将数据存储在堆上二避免栈上数据拷贝三将动态大小类型变为Sized固定大小类型四特征对象 三、Box内存布局一Veci32内存布局二VecBoxi32内存布局 四、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一引入原因二RcT三Arc 二、Cell与RefCell一引入原因二CellT三RefCellT四内部可变性五Rc RefCell组合使用六通过Cell::from_mut解决借用冲突 10.循环引用与结构体自引用一、循环引用一循环引用的产生二Weak解决循环引用三unsafe解决循环引用 二、结构体自引用一自引用结构体的问题二unsafe实现自引用三Pin实现自引用四ouroboros库实现自引用五其他相关库六Rc RefCell或Arc Mutex解决自引用七终极大法八学习资源推荐 11.并发与线程一、并发和并行一概念区别二编程语言的并发模型 二、使用线程一多线程编程的风险二创建线程三等待子线程的结束四在线程闭包中使用move五线程是如何结束的六多线程的性能七线程屏障Barrier八线程局部变量Thread Local Variable九用条件控制线程的挂起和执行十只被调用一次的函数 12. 线程间消息传递一、消息通道一多发送者单接收者mpsc1. 创建与使用2. 类型推导与所有权3. 循环接收与多发送者4. 消息顺序 二同步和异步通道1. 异步通道2. 同步通道 三关闭通道四传输多种类型的数据 二、新手容易遇到的坑一示例问题二解决办法 三、mpmc更好的性能一第三方库介绍1. crossbeam-channel2. 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_defaultm, K, V(map: m mut HashMapK, 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 fa, T(x: *const T) - a T {unsafe {*x}
}不安全代码解引用裸指针时产生无界生命周期。如上述代码中x无生命周期a T的a是无界生命周期它不受约束比static强大。应在函数声明中运用生命周期消除规则避免无界生命周期。
三、生命周期约束HRTB
一语法及含义
a: b表示a至少要活得跟b一样久如struct DoubleRefa,b:a, T {r: a T, s: b T}b必须活得比a久。T: a表示类型T必须比a活得要久如struct Refa, T: a {r: a T}新版本编译器可自动推导可简化为struct Refa, T {r: a T}。
二综合例子
struct ImportantExcerpta {part: a str,
}
impla: b, b ImportantExcerpta {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且ab。
四、闭包函数的消除规则
一问题示例
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 funT, F: Fn(T) - T(f: F) - F {f
}可使用Fn特征解决通过包装闭包解决生命周期问题。
五、NLLNon - 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块消除
impla Reader for BufReadera {// methods go here// impl内部实际上没有用到a
}如果impl块内部未用到a可写成impl Reader for BufReader__是匿名生命周期表示可忽略该生命周期。
二生命周期约束消除
// Rust 2015
struct Refa, T: a {field:a T
}// Rust 2018
struct Refa, T {field: a T
}新版本Rust中T: a可被消除可显式声明但影响可读性。
八、复杂例子分析
一代码及错误
struct Interfacea {manager: a mut Managera
}
impla Interfacea {pub fn noop(self) {println!(interface consumed);}
}struct Managera {text: a str
}
struct Lista {manager: Managera,
}
impla Lista {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方法的参数给予不同于Lista的生命周期b。
struct Interfaceb, a: b{manager:b mut Managera
}
implb,a:b Interfaceb,a{pub fn noop(self) {println!(interface consumed);}
}
struct Managera{text:a str
}
struct Lista{manager: Managera
}
impla Lista{pu fn get_interfaceb(b mut self) - Interfaceb,awhere 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_itT: 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_itT: Debug static(input: T) {println!({:?}, input);
}
fn main() {let i 5;print_it(i);
}在这个版本中函数print_it接受T作为参数。使用T时编译器不检查T本身的生命周期所以代码不会报错。
3. 另一个示例
fn static_boundT: 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跟 1x进行相加最终返回它们的和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 CacherT where T: Fn(u32) - u32 {query: T, value: Optionu32}。这里query为闭包类型受Fn(u32) - u32特征约束表示闭包有一个u32类型参数且返回u32类型值。
struct CacherT where T: Fn(u32) - u32 {query: T,value: Optionu32,
}二方法实现
实现了new方法用于创建Cacher实例value方法用于查询缓存值如果不存在则调用闭包query加载。
implT CacherT where T: Fn(u32) - u32 {fn new(query: T) - CacherT {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 CacherT,E
whereT: Fn(E) - E,E: Copy
{query: T,value: OptionE,
}
implT,E CacherT,E
whereT: Fn(E) - E,E: Copy
{fn new(query:T) - CacherT,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]: cant 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_onceF(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 execa, 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的前提是实现FnMutFnMut的前提是实现FnOnce。Fn获取selfFnMut获取mut selfFnOnce获取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分支返回不同闭包类型则报错。
三最终解决方案
使用Boxdyn Fn(i32) - i32作为返回类型通过Box方式将闭包装箱为特征对象来解决不同闭包类型返回的问题。
fn factory() - Boxdyn 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: Veci32 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: VecShoe, shoe_size: u32) - VecShoe {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) - OptionSelf::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 fooX: Trait(t: X) {}impla Trait for a i32 {}fn main() {let t: mut i32 mut 0;foo(t);
}二点操作符
方法调用的点操作符会发生类型转换包括自动引用、自动解引用、强制类型转换直到类型匹配等。
fn do_stuffT: 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(VecString);
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);二应用场景
简化ResultT, E枚举。
type ResultT std::result::ResultT, 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 Hows it going?;
// ok
let s3: str on?String和str底层堆数据的明确信息它才是固定 大小类型将动态类型数据固定化的秘决就是使用引用指向这些动态数据然后在引用中存储相关的内存位置、长度等信息。
二Sized特征
编译器自动为泛型函数添加T: Sized特征约束保证泛型参数是固定类型。
fn genericT: Sized(t: T) {// --snip--
}使用?Sized特征可在泛型函数中使用动态数据类型此时函数参数类型需变为T。
fn genericT:?Sized(t: T) {// --snip--
}Boxstr无法像封装特征对象那样简单封装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!(Couldnt convert {}, x),}
}在Rust 1.34后可实现TryFrom特征来做转换也可使用宏简化TryFrom特征的实现。不推荐但可行的方式是使用std::mem::transmute需注意底层类型大小控制。
6.BoxT堆对象分配
一、Rust中的堆栈
一堆栈概念
栈 内存从高位地址向下增长连续分配操作系统对其大小有限制main线程栈大小为8MB普通线程为2MB。函数调用时创建临时栈空间调用结束自动进入Drop流程栈顶指针自动移动无需手动干预申请和释放高效。 堆 内存从低位地址向上增长通常只受物理内存限制不连续。堆上对象有所有者受所有权规则限制赋值时发生所有权转移浅拷贝栈上引用或智能指针。
二堆栈性能
小型数据栈上分配和读取性能高于堆。中型数据栈上分配性能高读取性能与堆无区别无法利用寄存器或CPU高速缓存需内存寻址。大型数据建议在堆上分配和使用。
二、BoxT的使用场景
一将数据存储在堆上
fn main() {let a Box::new(3);println!(a {}, a); // 下面代码报错需显式解引用// let b a 1; // 正确写法let b *a 1;
}BoxT实现了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,
}// 使用BoxT解决
#![allow(unused)]
enum List {Cons(i32, BoxList),Nil,
}递归类型是动态大小类型DST使用BoxT可将其转换为固定大小类型。
四特征对象
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: VecBoxdyn Draw vec![Box::new(Button { id: 1 }), Box::for e in elems {e.draw()}
}特征对象可将不同类型包装成实现某特征的对象放入数组特征是DST类型特征对象将其转换为固定大小类型Boxdyn Drew就是特征对象。
三、Box内存布局
一Veci32内存布局
#![allow(unused)]
fn main() {
(stack) (heap)
┌──────┐ ┌───┐
│ vec1 │──→│ 1 │
└──────┘ ├───┤│ 2 │├───┤│ 3 │├───┤│ 4 │└───┘
}Vec智能指针存储在栈中指向堆上数组数据。
二VecBoxi32内存布局
#![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;。
⭕️注意
使用借用数组中的元素否则会报所有权错误。表达式不能隐式的解引用因此必须使用**做两次解引用第一次将Boxi32类型转成Boxi32第二次进一步转成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让智能指针像引用一样工作可通过*操作符解引用如BoxT智能指针。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 MyBoxT(T);
implT MyBoxT {fn new(x: T) - MyBoxT {MyBox(x)}
}定义了一个类似BoxT的智能指针MyBoxT通过MyBox::new创建。
二为自定义智能指针实现Deref特征
#![allow(unused)]
use std::ops::Deref;
implT Deref for MyBoxT {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特征sString类型传给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类型的值赋给它对于s2MyBox虽未实现to_string方法但通过Deref可调用该方法。
五、Deref规则总结
一基本规则
若T: DerefTargetU则foofoo为T类型对象会自动转换为U。
二引用归一化
Rust会对智能指针和多重引用做引用归一化操作转换成v形式再解引用。例如T会自动解引用为T然后T再自动解引用为T。
六、三种Deref转换
rust支持将一个可变的引用转换成另一个可变的引用将一个可变引用转换成一个不可变的引用
一不可变Deref转换
当T: DerefTargetU可将T转换成U。
二可变Deref转换
当T: DerefMutTargetU可将mut T转换成mut U。
三可变转不可变Deref转换
当T: DerefTargetU可将mut T转换成U但反之不行。
四示例
struct MyBoxT {v: T,
}
implT MyBoxT {fn new(x: T) - MyBoxT {MyBox { v: x }}
}
use std::ops::Deref;
implT Deref for MyBoxT {type Target T;fn deref(fn display(s: mut String) {s.push_str(world);println!(s {}, s);
}实现DerefMut必须先实现Deref特征。T: DerefMutTargetU将mut T类型转换为mut U类型。
七、总结
Deref是Rust中常见的隐式类型转换可连续实现如BoxString - 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创建一个新的RcString智能指针并赋给变量a此时引用计数为1。然后使用Rc::clone克隆一份智能指针RcString并赋给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最终智能指针和它指向的底层字符串都会被清理释放。
不可变引用特性 RcT是指向底层数据的不可变的引用因此无法通过它来修改数据。如果要修改数据需要配合其它数据类型如RefCellT或MutexT。 综合例子 考虑一个场景有很多小工具每个工具都有自己的主人但是存在多个工具属于同一个主人的情况此时使用RcT就非常适合。
use std::rc::Rc;
struct Owner {name: String,//...其它字段
}
struct Gadget {id: i32,owner: RcOwner,//...其它字段
}
fn main() {// 创建一个基于引用计数的 Owner.let gadget_owner: RcOwner 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),};// 释放掉第一个 RcOwnerdrop(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在多线程中的问题 在多线程场景中使用RcT会报错因为它不能在线程间安全的传递实际上是因为它没有实现Send特征且计数器没有使用任何并发原语无法实现原子化的计数操作最终会导致计数错误。
三Arc
基本原理 Arc是Atomic Rc的缩写是原子化的RcT智能指针。原子化是一种并发原语它能保证我们的数据能够安全的在线程间共享。 性能损耗 原子化或者其它锁虽然可以带来线程安全但都会伴随着性能损耗而且这种性能损耗还不小。所以Rust把这种选择权交给开发者因为需要线程安全的代码其实占比并不高大部分时候我们开发的程序都在一个个线程内。 与Rc的区别 Arc和Rc拥有完全一样的API修改起来很简单只需要把use std::rc::Rc改为use std::sync::Arc。两者的区别在于Arc是线程安全的可以用于多线程中共享数据而Rc只能用于同一线程内部。
二、Cell与RefCell
一引入原因
Rust编译器严格的所有权和借用规则在某些场景下缺乏灵活性Cell和RefCell用于内部可变性即可以在拥有不可变引用的同时修改目标数据。
二Cell
基本原理 CellT适用于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
基本原理 RefCellT用于引用类型它将借用规则从编译期推迟到程序运行期。违背规则会导致运行时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: RefCellVecString,
}
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);
}上述代码中使用RefCellString包裹一个字符串同时通过Rc创建了它的三个所有者s、s1和s2并且通过其中一个所有者s2对字符串内容进行了修改。由于Rc的所有者们共享同一个底层的数据因此当一个所有者修改了数据时会导致全部所有者持有的数据都发生了变化。
性能分析 内存损耗从对内存的影响来看仅仅多分配了三个usize/isize并没有其它额外的负担。CPU损耗 对RcT解引用是免费的编译期但是*带来的间接取值并不免费。克隆RcT需要将当前的引用计数跟0和usize::Max进行一次比较然后将计数值加1。释放dropRcT需要将计数值减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 Veci32) {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 Veci32) {let slice: [Celli32] 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]转换为[CellT]解决问题。
10.循环引用与结构体自引用
一、循环引用
一循环引用的产生
示例代码分析
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;#[derive(Debug)]
enum List {Cons(i32, RefCellRcList),Nil,
}
impl List {fn tail(self) - OptionRefCellRcList {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和RefCellRcList。在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计数为1b创建后a的rc计数变为2b的初始化rc计数为1经过一些操作后a和b的计数最终都为2。导致的问题 循环引用可能会使程序不断分配内存、泄漏内存最终导致OOM。如果尝试打印循环引用的内容还可能造成栈溢出如下所示
// 反注释这行代码会导致栈溢出
// println!(a next item {:?}, a.tail());二Weak解决循环引用
Weak的特点 Weak类似于Rc但不持有所有权不增加引用计数仅保存指向数据的弱引用。通过upgrade方法访问数据返回OptionRcT若引用的值不存在则返回None。 与Rc的对比
WeakRc不计数引用计数不拥有所有权拥有值的所有权不阻止值被释放(drop)所有权计数归零才能drop引用的值存在返回Some不存在返回None引用的值必定存在通过upgrade取到OptionRcT然后再取值通过Deref自动解引用取值无需任何操作
使用示例 工具间的故事
use std::rc::Rc;
use std::rc::Weak;
use std::cell::RefCell;struct Owner {name: String,gadgets: RefCellVecWeakGadget,
}
struct Gadget {id: i32,owner: RcOwner,
}
fn main() {let gadget_owner: RcOwner 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结构体包含RefCellVecWeakGadgetGadget结构体包含RcOwner通过Weak避免循环引用。tree数据结构
use std::cell::RefCell;
use std::rc::{Rc, Weak};#[derive(Debug)]
struct Node {value: i32,parent: RefCellWeakNode,children: RefCellVecRcNode,
}
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结构体包含RefCellWeakNode父节点引用和RefCellVecRcNode子节点引用通过Weak和Rc的配合避免循环引用。
三unsafe解决循环引用
方法介绍 可以使用unsafe里的裸指针解决循环引用问题虽然不安全但性能高、代码简单符合直觉。 示例说明 文中未详细展开只提供了相关源码链接。
二、结构体自引用
一自引用结构体的问题
简单示例及报错
#![allow(unused)]
fn main() {struct SelfRefa {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 WhatAboutThisa {name: String,nickname: Optiona str,
}
fn creatora() - WhatAboutThisa {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: NonNullString,_pin: PhantomPinned,
}impl Unmovable {fn new(data: String) - PinBoxSelf {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: Pinmut 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的NonNullString指针和PhantomPinned。通过PinBoxSelf确保数据所有权不会被转移在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!(Heres 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: RefCellu32 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返回MutexGuardT智能指针。MutexGuardT实现Deref和Drop特征可自动解引用获取数据且超出作用域自动释放锁。注意事项 数据被Mutex拥有需获取锁才能访问内部数据lock方法可能因持有锁的线程panic而报错。
二多线程中使用Mutex
RcT的问题
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());
}不能用RcT实现多所有权因为RcT无法在线程中安全传输未实现Send特征。ArcT的使用
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());
}需用ArcT实现多所有权结合ArcT和MutexT可实现多线程的内部可变性RcT/RefCellT用于单线程内部可变性。
三使用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: Mutexi64 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: MutexGuardi64 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: Mutexi64 Mutex::new(0);static ref MUTEX2: Mutexi64 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: MutexGuardi64 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方法会接收一个MutexGuarda, T且它会自动地暂时释放这个锁使其他线程可以拿到锁并进行放飞自我的相关研究和实践是非常有趣和有意义的因为它涉及到人类对自由、快乐和满足的追求以及如何在社会和道德的框架内实现这些追求。和进行数据更新。// 同时当前线程在此处会被阻塞直到被其他地方notify后它会将原本的MutexGuarda, 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循环冲突时需等待。CASCompare 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 6Main部分执行后A操作再执行Main剩余部分、Y 2Main执行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转移到子线程会报错Rci32无法在线程间安全转移因为Rci32未实现Send特征。
二Rc和Arc源码对比
#![allow(unused)]
fn main() {
// Rc源码片段
implT:?Sized!marker::Send for RcT {}
implT:?Sized!marker::Sync for RcT {}// Arc源码片段
unsafe implT:?Sized Sync Send Send for ArcT {}
unsafe implT:?Sized Sync Send Sync for ArcT {}
}RcT的Send和Sync特征被特地移除实现而ArcT实现了Sync Send。Send和Sync是在线程间安全使用一个值的关键。
二、Send和Sync特征
一特征作用
Send实现Send的类型可以在线程间安全地传递其所有权。Sync实现Sync的类型可以在线程间安全地共享通过引用。一个类型要在线程间安全共享的前提是指向它的引用必须能在线程间传递。若类型T的引用T是Send则T是Sync。
二RwLock和Mutex的实现对比
#![allow(unused)]
fn main() {
unsafe implT:?Sized Send Sync Sync for RwLockT {}
}RwLock可以在线程间安全共享实现了Sync且其中的值T也必须能在线程间共享所以T有Sync特征约束。
#![allow(unused)]
fn main() {
unsafe implT:?Sized Send Sync for MutexT {}
}MutexT中的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: MutexString 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: MutexString 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: Optionmut 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: Optionmut Config None;
fn init() - Optionstatic 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: SyncOnceCellLogger SyncOnceCell::new();
// Rust 1.70版本以上
// static LOGGER: OnceLockLogger 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: Optionstr None;let o1: Resultstr, str Ok(ok1);let o2: Resultstr, str Ok(ok2);let e1: Resultstr, str Err(error1);let e2: Resultstr, 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: Optionstr 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: Resultstr, str Ok(ok1);let o2: Resultstr, str Ok(ok2);let fn_ok |_| Ok(ok2); let e1: Resultstr, str Err(error1);let e2: Resultstr, 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: Optionstr 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: Resultstr, str Ok(ok1);let o2: Resultstr, str Ok(ok2);let fn_ok |_| Ok(ok2); let e1: Resultstr, str Err(error1);let e2: Resultstr, 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中的值。
fn main() {let s1 Some(abcde);let s2 Some(5);let n1: Optionstr None;let n2: Optionusize None;let o1: Resultstr, str Ok(abcde);let o2: Resultusize, str Ok(5);let e1: Resultstr, str Err(abcde);let e2: Resultusize, 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: Resultstr, str Ok(abcde);let o2: Resultstr, isize Ok(abcde);let e1: Resultstr, str Err(404);let e2: Resultstr, 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类似通过闭包提供默认值。
fn main() {const V_DEFAULT: u32 1;let s: Resultu32, () Ok(10);let n: Optionu32 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: Optioni8 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: Optionstr None;let o: Resultstr, str Ok(abcde);let e: Resultstr, 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: Optionstr None;let fn_err_message || error message;let o: Resultstr, str Ok(abcde);let e: Resultstr, 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 FromT: Sized {fn from(_: T) - Self;}
}二实现From特征示例
实现Fromio::Error特征将io::Error转换为自定义AppError。
use std::fs::File;
use std::io;
#[derive(Debug)]
struct AppError {kind: String,message: String,
}
// 实现 Fromio::Error
use std::fs::File;
use std::io;
#[derive(Debug)]
struct AppError {kind: String, // 错误类型message: String, // 错误信息
}
// 为 AppError 实现 std::convert::From 特征由于 From 包含在 std::prelude 中因此可以直接简化引入。
// 实现 Fromio::Error 意味着我们可以将 io::Error 错误转换成自定义的 AppError 错误
impl Fromio::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 Fromio::Error for AppError {fn from(error: io::Error) - Self {AppError {kind: String::from(io),message: error.to_string(),}}
}
impl Fromnum::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() - ResultString, std::io::Error {let file std::env::var(MARKDOWN)?;let source read_to_string(file)?;Ok(source)
}二解决方式
使用特征对象Boxdyn Error 简单但有局限性Result不限制错误类型时可能无法使用。
use std::fs::read_to_string;
use std::error::Error;
fn main() - Result(), Boxdyn Error {let html render()?;println!({}, html);Ok(())
}
fn render() - ResultString, Boxdyn Error {let file std::env::var(MARKDOWN)?;let source read_to_string(file)?;Ok(source)
}18.语言中的unsafe关键字
一、unsafe简介
一存在原因
编译器限制 Rust的静态检查很强且保守一些正确代码可能因编译器无法分析其正确性而被拒绝例如自引用相关的编译检查很难绕过此时可使用unsafe解决。
// 自引用相关代码可能需要使用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必不可少。
二使用原则
没必要用时不用必要时大胆用但要控制好边界让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: Boxi32 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-bindgen和cbindgen 用于FFI调用保证接口正确性rust-bindgen用于在Rust中访问C代码cbindgen反之可自动生成相应接口。 cxx 用于跟C提出假设并进行验证是科学研究中的重要方法通过合理设计实验和收集数据可以对假设进行支持或反驳从而推动科学知识的发展。和C代码交互提供双向调用且安全无需通过unsafe使用。 Miri 可生成Rust的中间层表示MIR检查常见未定义行为如内存越界、使用未初始化数据、数据竞争、内存对齐问题等但只能识别被执行代码路径的风险。 Clippy 官方检查器提供有限的unsafe支持如missing_safety_docs检查可检查unsafe函数是否遗漏文档。 Prusti 需要构建证明来检查代码中的不变量是否正确使用在安全代码中中微子的性质和它们在宇宙中的作用是当前物理学研究的热点话题之一科学家们通过各种实验和观测来探索中微子的奥秘。和在安全代码中使用不安全不变量时有用。 模糊测试相关 Rust Fuzz Book列出一些模糊测试方法还可使用rutenspitz过程宏测试有状态代码。
四、内联汇编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()) - PollSelf::Output方法Poll枚举包含Ready和Pending。
trait SimpleFuture {type Output;fn poll(mut self, wake: fn()) - PollSelf::Output;
}
enum PollT {Ready(T),Pending,
}工作原理 通过poll方法推进Future执行若在当前poll中可完成则返回Poll::Ready(result)反之返回Poll::Pending并安排wake函数当Future准备好进一步执行时wake函数被调用执行器再次调用poll方法。 示例 以从socket读取数据为例说明Future的工作方式SocketRead结构体是一个Future。
pub struct SocketReada {socket: a Socket,
}
impl SimpleFuture for SocketRead_ {type Output Vecu8;fn poll(mut self, wake: fn()) - PollSelf::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变成了Pinmut Selfwake: fn()修改为mut Context_Pin可创建无法被移动的FutureContext类型通过Waker类型的值唤醒特定任务。
二使用Waker来唤醒任务
Waker提供wake()方法用于告诉执行器任务可被唤醒执行器可对相应Future再次进行poll操作。
三构建一个定时器
定时器Future的实现 以构建一个简单的定时器Future为例使用ArcMutexT在新线程和Future定时器间共享状态通过检查共享状态确定定时器是否完成若未完成则设置Waker新线程在睡眠结束后可唤醒任务。
pub struct TimerFuture {shared_state: ArcMutexSharedState,
}struct SharedState {completed: bool,waker: OptionalWaker,
}impl Future for TimerFuture {type Output ();fn poll(self: Pinmut Self, cx: mut Context_) - PollSelf::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 FutureOutput () 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: ArcSelf) {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: Pinmut Self。当async语句块包含引用类型时移动Future可能使引用非法固定Future位置可避免。
三Unpin
多数类型自动实现Unpin特征表示可安全移动。Pin是结构体如Pinmut T确保T不被移动可被Pin的值实现!Unpin特征。
四深入理解Pin
自引用类型示例
以Test结构体为例包含aString和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: Pinmut Self) {let self_ptr: *const String self.a;let this unsafe { self.get_unchecked_mut() };this.b self_ptr;}fn a(self: PinSelf) - str {self.get_ref().a}fn b(self: PinSelf) - 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) - PinBoxSelf {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: PinSelf) - str {self.get_ref().a}fn b(self: PinSelf) - 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 FutureOutput () 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: UnpinPina, T与a mut T相同Pin无效果。多数标准库类型实现Unpinasync/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()返回FutureOutput u8async fn foo() - u8 { 5 }fn bar() - impl FutureOutput 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_expandeda(x: a u8) - impl FutureOutput u8 a {async move { *x }
}#![allow(unused)]
fn main() {use std::future::Future;async fn borrow_x(x: u8) - u8 { *x }fn good() - impl FutureOutput 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 FutureOutput () {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: Pinmut Self, cx: mut Context_) - PollOptionSelf::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但返回FutureOutput OptionT需.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: Pinmut dyn StreamItem 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: Pinmut dyn StreamItem Resulti32, io::Error,) - Resulti32, 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: Pinmut dyn StreamItem Resultu8, 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() - ResultBook, () { /* ... */ Ok(Book) }
async fn get_music() - ResultMusic, 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状态时立即执行。
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必须的。Unpinselect通过可变引用使用Future未完成的Future所有权可被其他代码使用。FusedFutureFuture完成后select不能再轮询Fuse相当于熔断完成后poll返回Poll::Pending。
#![allow(unused)]
fn main() {
use futures::{stream::{Stream, StreamExt, FusedStream},select,
};async fn add_two_streams(mut s1: impl StreamItem u8 FusedStream Unpin,mut s2: impl StreamItem 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 StreamItem () 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语句块无法显式声明返回值与?一起使用时编译器无法推断ResultT, E中E的类型。 示例
async fn foo() - Resultu8, String {Ok(1)
}
async fn bar() - Resultu8, 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() - BoxFuturestatic, () {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函数被调用时会产生一次堆内存分配高频调用时需注意性能。