96SEO 2026-02-20 02:13 23
二、无界生命周期三、生命周期约束HRTB一语法及含义二综合例子

static一、static的常见情况一字符串字面量二特征对象
3.闭包笔记一、闭包的定义和基本使用一定义二语法形式三类型推导
二、闭包用于简化代码一传统函数实现问题二函数变量实现的不足三闭包实现的优势
四、闭包捕获作用域中的值一闭包的特性二函数的限制三闭包对内存的影响
六、三种Fn特征一FnOnce特征二FnMut特征三Fn特征四三种特征的关系
七、闭包作为函数返回值一返回闭包的问题二解决方法三最终解决方案
4.迭代器Interator一、迭代器基础一迭代器与for循环二IntoIterator特征三迭代器的惰性初始化四next方法
二、迭代器相关方法和类型转换一into_iter、iter和ter_mut二Iterator和IntoIterator的区别
四、实现Iterator特征一自定义迭代器示例二Iterator特征的其它方法
五、变形记Transmutes一mem::transmute二mem::transmute_copy
八、!永不返回类型九、Sized和不定长类型DST一不定长类型DST二Sized特征
6.BoxT堆对象分配一、Rust中的堆栈一堆栈概念二堆栈性能
二、BoxT的使用场景一将数据存储在堆上二避免栈上数据拷贝三将动态大小类型变为Sized固定大小类型四特征对象
三、Box内存布局一Veci32内存布局二VecBoxi32内存布局
7.Deref解引用一、Deref的引入一问题示例二智能指针与Deref
二、常规引用解引用三、智能指针解引用一自定义智能指针二为自定义智能指针实现Deref特征三*背后的原理
四、函数和方法中的隐式Deref转换一基本隐式转换二连续隐式转换三在方法、赋值中的应用
六、三种Deref转换一不可变Deref转换二可变Deref转换三可变转不可变Deref转换四示例
8.Drop释放资源一、Drop特征的作用二、Drop示例一结构体的Drop实现二未实现Drop的结构体
三、Drop顺序四、手动回收一手动drop的问题二正确的手动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
二、使用线程一多线程编程的风险二创建线程三等待子线程的结束四在线程闭包中使用move五线程是如何结束的六多线程的性能七线程屏障Barrier八线程局部变量Thread
Variable九用条件控制线程的挂起和执行十只被调用一次的函数
二、互斥锁Mutex一单线程中使用Mutex二多线程中使用Mutex三使用Mutex的注意事项
四、条件变量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
二、自定义错误类型一实现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
这本书为基础的记录学习笔记。
有关这本书更多的详细可以网购或专卖店去详细了解关于rust入门基础的文章有。
foo.mutate_and_share();foo.share();println!({:?},
}理论上mutate_and_share最终是不可变借用share也是不可变借用应编译通过但实际报错cannot
mutable。
原因是生命周期消除规则使mutate_and_share中mut
self和self生命周期相同导致可变借用在main函数作用域内有效使share无法再进行不可变借用。
V::default());map.get_mut(key).unwrap()}}
time。
原因是编译器认为对map的可变借用持续到match语句块结束而实际在map.get_mut(key)调用完成后可变借用就可结束导致后续借用失败。
}不安全代码解引用裸指针时产生无界生命周期。
如上述代码中x无生命周期a
T的a是无界生命周期它不受约束比static强大。
应在函数声明中运用生命周期消除规则避免无界生命周期。
b约束才能编译因为self.part生命周期与self一致a需转换为b且ab。
};fn_elision能编译closure_slision报错lifetime
enough原因是编译器无法推测返回引用和传入引用谁活得更久。
函数生命周期体现在签名引用类型上编译器可分析消除规则闭包生命周期分散在参数和闭包体中编译器难以分析所以针对函数和闭包有不同消除规则。
旧规则引用生命周期从借用开始到作用域结束新规则1.31版本引入引用生命周期从借用处开始到最后一次使用的地方结束。
s;按旧规则报错按新规则r1和r2在println!后生命周期结束r3借用不违反规则。
hello}};list.get_interface().noop();println!(Interface
但是Interface在之前调用完成后就应该被释放了use_list(list);
get_interface方法中参数生命周期a与List生命周期相同导致可变借用持续到main函数结束无法再进行借用。
为get_interface方法的参数给予不同于Lista的生命周期b。
hello}};list.get_interface().noop();println!(Interface
下面的调用可以通过因为Interface的生命周期不需要跟list一样长use_list(list);
Clemens;字符串字面量具有static生命周期它在程序的整个运行期间都存在。
一个引用必须要活得跟剩下的程序一样久才能被标注为static。
World”的生命周期是static但变量string的生命周期取决于函数作用域。
虽然static引用本身可以和程序活得一样久但持有该引用的变量受其作用域的限制。
在某些情况下与static有相同的约束即T必须活得和程序一样久。
}这里直接使用T作为参数当print_it函数被调用时i作为input由于i的生命周期不是static所以会报错。
}在这个版本中函数print_it接受T作为参数。
使用T时编译器不检查T本身的生命周期所以代码不会报错。
String.to_string();static_bound(s1);
static针对引用要求引用指向的数据活得跟程序一样久而引用本身遵循作用域范围。
T:
static主要约束类型T当使用其引用T时编译器可能不检查T本身的生命周期。
闭包是一种匿名函数它可以赋值给变量或作为参数传递给其他函数并且能够捕获调用者作用域中的值。
Rust可以自动推导闭包的参数和返回值类型。
但如果只声明了闭包而未使用可能需要标注类型。
与函数不同闭包通常不对外作为
虽然类型推导很好用但是它不是泛型当编译器推导出一种类型后它就会一直使用该类型
example_closure(String::from(hello));
以健身代码为例如果使用传统函数实现健身动作当需要修改函数调用的声音或动作次数等内容时需要在多处修改代码。
如果将函数赋值给变量后调用虽然可以通过修改变量赋值来改变调用的函数但如果函数内部参数发生变化仍然需要在多处修改调用处的代码。
闭包可以捕获相关变量。
例如在健身代码中闭包action可以捕获intensity这样只需修改闭包内部的实现而无需在多处修改调用处的代码。
Optionu32}。
这里query为闭包类型受Fn(u32)
u32特征约束表示闭包有一个u32类型参数且返回u32类型值。
实现了new方法用于创建Cacher实例value方法用于查询缓存值如果不存在则调用闭包query加载。
当前设计只支持u32类型的值可以将u32替换为泛型E以支持更多类型。
call_with_different_values(){let
闭包从环境中捕获值时会分配内存存储而函数不会这在某些场景下可能会成为一种负担。
}可以使用move关键字强制闭包取得捕获变量的所有权常用于闭包生命周期大于捕获变量生命周期的情况。
在这里s的作用域结束但是闭包仍然可以使用它所拥有的s的副本//
s.push_str(str);如果闭包未声明为可变会报错需要使用mut关键字修饰闭包。
即使闭包未用mut修饰但从特征类型系统和语言修饰符两方面可以保障程序正确运行。
例如当exec函数接收可变类型闭包时即使闭包看似不可变但实际是可变类型由rust
analyzer可看出实现了FnMut特征。
闭包自动实现Copy特征的规则是只要闭包捕获的类型都实现了Copy特征则闭包默认实现Copy特征。
例如取得可变引用的闭包未实现Copy特征。
s.push_str(str);exec(update_string);println!({:?},s);
str);在闭包中只对s进行不可变借用。
如果闭包实现的是FnMut特征但在使用时标注为Fn特征会报错需要正确标注特征。
所有闭包都自动实现FnOnce特征至少可被调用一次。
没有移出所捕获变量的所有权的闭包自动实现FnMut特征不需要对捕获变量进行改变的闭包自动实现Fn特征。
从特征约束看Fn的前提是实现FnMutFnMut的前提是实现FnOnce。
Fn获取selfFnMut获取mut
selfFnOnce获取self。
在实际项目中建议先使用Fn特征让编译器提示正误及如何选择。
示例
i32作为返回类型可解决但有局限只能返回同样类型的闭包。
若if和else分支返回不同闭包类型则报错。
i32作为返回类型通过Box方式将闭包装箱为特征对象来解决不同闭包类型返回的问题。
在Rust中for循环是编译器提供的语法糖用于遍历迭代器中的元素。
与其他语言如JavaScript的for循环不同Rust的for循环不依赖索引来访问集合中的元素。
实现IntoIterator特征的类型可以通过into_iter方法转换为迭代器。
数组、数值序列等都实现了IntoIterator特征。
迭代器是惰性的创建迭代器时不会发生任何迭代行为只有在使用时才会开始迭代。
迭代器通过实现Iterator特征的next方法来获取下一个元素。
next方法返回Option类型有元素时返回Some(Item)无元素时返回None。
手动迭代时需要将迭代器声明为mut。
into_iter会夺走所有权iter是借用iter_mut是可变借用。
values);二Iterator和IntoIterator的区别
Iterator是迭代器特征只有实现了它才能称为迭代器并调用next方法。
IntoIterator强调一个类型实现该特征后可以通过into_iter等方法变成一个迭代器。
消费者是迭代器上依赖next方法消费元素并返回值的方法。
例如sum方法是消费者适配器它会拿走迭代器的所有权并对元素进行求和。
迭代器适配器返回一个新的迭代器是实现链式方法调用的关键且是惰性的需要消费者适配器收尾。
例如map方法是迭代器适配器collect方法是消费者适配器。
可以为自定义类型实现Iterator特征来创建迭代器。
以Counter结构体为例实现Iterator特征并定义next方法来实现计数逻辑。
Iterator特征中除next外的方法有默认实现且基于next方法。
例如zip、map、filter是迭代器适配器sum是消费者适配器可以组合使用。
zip将两个迭代器合成一个迭代器每次迭代返回一个元组map对迭代器的每个元素应用一个函数返回一个新的迭代器filter给定的条件过滤迭代器中的元素返回一个新的迭代器sum计算迭代器中所有元素的总和返回一个单一的值
Counter::new().zip(Counter::new().skip(1)).map(|(a,
enumerate是迭代器适配器它为迭代器元素添加索引产生形如(索引值)的元组形式的新迭代器。
通过测试迭代器和for循环在完成求和任务时迭代器性能更快一些。
迭代器是零成本抽象不会引入运行时开销编译器可进行优化。
使用as操作符进行基本类型转换。
需要注意数据范围避免将大类型转换为小类型导致错误。
将字符a转换为整数97println!({},{},{},a,b,c)
try_into会返回Result类型可以对转换错误进行处理。
可以通过简单方式将一个结构体转换为另一个结构体但存在更通用的方式。
方法调用的点操作符会发生类型转换包括自动引用、自动解引用、强制类型转换直到类型匹配等。
}上述代码中编译器首先检查能否进行值方法调用因为T实现了Clone特征所以可以进行值方法调用cloned的类型是T。
非常危险的转换方式将类型T直接转成类型U要求两个类型占用同样大小字节数。
将裸指针转换为函数指针std::mem::transmute::*const
0);可能导致创建任意类型实例混乱、重载返回类型、未定义行为如将变形为mut等问题。
比mem::transmute更危险从T类型中拷贝出U类型所需字节数并转换但不检查大小U尺寸大于T时是未定义行为。
使用元组结构体将已有类型包裹可提供更有意义和可读性的类型名解决某些场景问题隐藏内部类型细节为外部类型实现外部特征。
Wrapper(vec![String::from(hello),
String::from(world)]);println!(w
是某一个类型的别名不是独立全新类型编译器仍将其当作原类型使用。
可简化代码提高可读性但不能实现为外部类型实现外部特征等功能。
类型别名并不是一个独立的全新的类型而是某一个类型的别名
panic的返回值是!代表函数永不返回任何值可用于解决match分支类型不匹配问题。
包括动态大小数组、切片、str、特征对象等。
这些类型大小在编译时无法得知需通过间接方式使用。
}上述代码中动态大小数组会报错因为n在编译期无法得知。
自然类型就变成了unized。
它是一个动态类型不是String动态字符串也不三str字符串切片它同时还是String和str的底层数据类型。
在运行期才知道所以在定义期间会报错
大小类型将动态类型数据固定化的秘决就是使用引用指向这些动态数据然后在引用中存储相关的内存位置、长度等信息。
}使用?Sized特征可在泛型函数中使用动态数据类型此时函数参数类型需变为T。
}Boxstr无法像封装特征对象那样简单封装str需使用into方法让编译器自动转换。
在C语言中实现简单通过enum定义和if判断实现整数与枚举的匹配。
使用三方库如num-traits和num-derive通过FromPrimitive特征实现转换。
1.34后可实现TryFrom特征来做转换也可使用宏简化TryFrom特征的实现。
不推荐但可行的方式是使用std::mem::transmute需注意底层类型大小控制。
内存从高位地址向下增长连续分配操作系统对其大小有限制main线程栈大小为8MB普通线程为2MB。
函数调用时创建临时栈空间调用结束自动进入Drop流程栈顶指针自动移动无需手动干预申请和释放高效。
内存从低位地址向上增长通常只受物理内存限制不连续。
堆上对象有所有者受所有权规则限制赋值时发生所有权转移浅拷贝栈上引用或智能指针。
小型数据栈上分配和读取性能高于堆。
中型数据栈上分配性能高读取性能与堆无区别无法利用寄存器或CPU高速缓存需内存寻址。
大型数据建议在堆上分配和使用。
}BoxT实现了Deref和Drop特征。
println!能正常打印是因为隐式调用Deref解引用let
1报错需用*操作符显式解引用a的智能指针在main函数结束时释放。
}递归类型是动态大小类型DST使用BoxT可将其转换为固定大小类型。
}特征对象可将不同类型包装成实现某特征的对象放入数组特征是DST类型特征对象将其转换为固定大小类型Boxdyn
}智能指针vec2存储在栈上指向堆上数组数组元素是Box智能指针Box又指向实际值。
从数组取元素时需对Box解引用如let
使用借用数组中的元素否则会报所有权错误。
表达式不能隐式的解引用因此必须使用**做两次解引用第一次将Boxi32类型转成Boxi32第二次进一步转成i32
消费Box并强制目标值从内存中泄漏可将String类型转为static生命周期的str类型。
String::new();s.push_str(hello,
world);Box::leak(s.into_boxed_str())
}使用场景运行期初始化的值需全局有效时可使用如存储配置的结构体实例在运行期动态插入内容后转为全局有效Box::leak性能高于Rc/Arc。
要知道真正具有static生命周期的往往都是编译期就创建的值被打包到二进制可执行文件中在整个程序中存活得都一样久。
Person却能与Person类型匹配这是因为Deref特征的作用。
智能指针实现了Deref和Drop特征。
Deref让智能指针像引用一样工作可通过*操作符解引用如BoxT智能指针。
Drop用于指定智能指针超出作用域后自动执行的代码。
}y是常规引用包含x的内存地址通过*y可获取x的值。
若执行assert_eq!(5,
}定义了一个类似BoxT的智能指针MyBoxT通过MyBox::new创建。
}实现Deref特征后MyBox智能指针可通过*解引用返回元组结构体中的元素self.0。
对智能指针Box解引用时实际调用*(y.deref())先调用deref方法返回常规引用再通过*解引用获取目标值。
这样做是因为所有权系统若deref直接返回值会转移所有权而我们不希望这样。
}String实现了Deref特征sString类型传给display函数时自动转换为str。
}使用自定义智能指针MyBox通过连续隐式转换变成str类型先Deref成String再Deref成str。
}对于s1通过两次Deref将str类型的值赋给它对于s2MyBox虽未实现to_string方法但通过Deref可调用该方法。
DerefTargetU则foofoo为T类型对象会自动转换为U。
Rust会对智能指针和多重引用做引用归一化操作转换成v形式再解引用。
例如T会自动解引用为T然后T再自动解引用为T。
rust支持将一个可变的引用转换成另一个可变的引用将一个可变引用转换成一个不可变的引用
Deref是Rust中常见的隐式类型转换可连续实现如BoxString
str的隐式转换。
原则上只应为自定义智能指针实现Deref特征。
在Rust中Drop特征用于自动和手动释放资源及执行指定的收尾工作。
它是智能指针的必备特征之一使得Rust无需GC和手动资源回收。
}每个结构体都可以实现Drop特征其中drop方法借用目标的可变引用。
输出结果为Running!、Dropping
HasDrop2!符合Drop顺序规则变量级别按逆序结构体内部按顺序。
即使结构体未实现Drop特征其内部字段若实现了Drop仍会调用drop方法。
变量级别按照逆序方式先创建的变量后被drop。
结构体内部按照定义顺序依次drop。
Foo;foo.drop();println!(Running!:{:?},
}直接调用Drop特征的drop方法是不允许的会报错。
因为Drop::drop只是借用可变引用提前调用drop后代码仍可能访问不存在的值不安全。
}应使用std::mem::drop函数进行手动drop它会拿走目标值的所有权保证后续使用会导致编译错误从而保证安全。
回收内存资源在绝大多数情况下Rust会自动回收内存资源但对于文件描述符、网络socket等特殊资源超出作用域时需手动drop以释放资源。
执行收尾工作如在结构体中实现Drop特征可在drop方法中执行一些自定义的收尾工作。
}一个类型不能同时实现Copy和Drop特征因为实现Copy的类型会被隐式复制难以预测析构函数执行时间和频率。
Rust所有权机制要求一个值只有一个所有者但在某些场景下会有问题比如图数据结构中多个边可能拥有同一个节点多线程中多个线程可能持有同一个数据但无法同时获取可变引用。
所以引入Rc和Arc来实现多个所有者共享一个数据Rc适用于单线程Arc适用于多线程。
counting的英文缩写它通过记录一个数据被引用的次数来确定该数据是否正在被使用。
当引用次数归零时就代表该数据不再被使用因此可以被清理释放。
它适用于在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束的情况。
Rc::strong_count(a));assert_eq!(Rc::strong_count(a),
}如上述代码使用Rc::new创建一个新的RcString智能指针并赋给变量a此时引用计数为1。
然后使用Rc::clone克隆一份智能指针RcString并赋给b引用计数增加到2。
a和b共享底层的字符串数据clone操作只是复制智能指针并增加引用计数并没有克隆底层数据是一种高效的浅拷贝。
可以使用关联函数Rc::strong_count来获取当前引用计数的值。
例如
Rc::strong_count(c));}println!(count
}变量c在语句块内部声明当离开语句块时它会因为超出作用域而被释放所以引用计数会减少1。
当a、b超出作用域后引用计数会变成0最终智能指针和它指向的底层字符串都会被清理释放。
RcT是指向底层数据的不可变的引用因此无法通过它来修改数据。
如果要修改数据需要配合其它数据类型如RefCellT或MutexT。
考虑一个场景有很多小工具每个工具都有自己的主人但是存在多个工具属于同一个主人的情况此时使用RcT就非常适合。
gadget1.owner.name);println!(Gadget
在多线程场景中使用RcT会报错因为它不能在线程间安全的传递实际上是因为它没有实现Send特征且计数器没有使用任何并发原语无法实现原子化的计数操作最终会导致计数错误。
Rc的缩写是原子化的RcT智能指针。
原子化是一种并发原语它能保证我们的数据能够安全的在线程间共享。
原子化或者其它锁虽然可以带来线程安全但都会伴随着性能损耗而且这种性能损耗还不小。
所以Rust把这种选择权交给开发者因为需要线程安全的代码其实占比并不高大部分时候我们开发的程序都在一个个线程内。
std::sync::Arc。
两者的区别在于Arc是线程安全的可以用于多线程中共享数据而Rc只能用于同一线程内部。
Rust编译器严格的所有权和借用规则在某些场景下缺乏灵活性Cell和RefCell用于内部可变性即可以在拥有不可变引用的同时修改目标数据。
CellT适用于T实现Copy的情况。
它通过get方法取值set方法设置新值。
如果尝试在Cell中存放未实现Copy的类型如String编译器会立刻报错。
RefCellT用于引用类型它将借用规则从编译期推迟到程序运行期。
违背规则会导致运行时panic。
RefCell::new(String::from(hello,
}上述代码在编译期不会报任何错误但运行时会因为违背了借用规则导致panic。
RefCell有一点运行期开销因为它包含了一个字节大小的“借用状态”指示器该指示器在每次运行时借用时都会被修改进而产生一点开销。
内部可变性是指对一个不可变的值进行可变借用这不符合Rust的基本借用规则。
例如
}上述代码会报错因为不能对一个不可变的值进行可变借用。
但在某些场景中一个值可以在其方法内部被修改同时对于其它代码不可变是很有用的。
{self.msg_cache.borrow_mut().push(msg)}
RefCell::new(Vec::new()),};mq.send(hello,
}在实现Messenger特征的send方法中通过RefCell包裹msg_cache实现对不可变引用中数据的修改。
Rc::new(RefCell::new(我很善变还拥有多个主人.to_string()));let
s.borrow_mut();s2.borrow_mut().push_str(,
yeah!);println!({:?}\n{:?}\n{:?},
}上述代码中使用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。
}使用Cell::from_mut和Cell::as_slice_of_cells方法将mut
RefCell::new(Rc::new(Nil))));let
RefCell::new(Rc::clone(a))));if
}上述代码定义了List枚举类型其中Cons变体包含i32和RefCellRcList。
在main函数中首先创建了a然后基于a创建了b接着通过RefCell的可变性让a和b相互引用形成循环引用。
引用计数问题
Rc::strong_count(a));循环引用导致a和b的引用计数在main函数结束前均为2不会归零。
例如a的初始化rc计数为1b创建后a的rc计数变为2b的初始化rc计数为1经过一些操作后a和b的计数最终都为2。
导致的问题
循环引用可能会使程序不断分配内存、泄漏内存最终导致OOM。
如果尝试打印循环引用的内容还可能造成栈溢出如下所示
Weak类似于Rc但不持有所有权不增加引用计数仅保存指向数据的弱引用。
通过upgrade方法访问数据返回OptionRcT若引用的值不存在则返回None。
WeakRc不计数引用计数不拥有所有权拥有值的所有权不阻止值被释放(drop)所有权计数归零才能drop引用的值存在返回Some不存在返回None引用的值必定存在通过upgrade取到OptionRcT然后再取值通过Deref自动解引用取值无需任何操作
RefCell::new(Vec::new()),});let
gadget_owner.clone()});gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(gadget1));gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(gadget2));for
gadget_owner.gadgets.borrow().iter()
gadget_opt.upgrade().unwrap();println!(Gadget
}在这个例子中Owner结构体包含RefCellVecWeakGadgetGadget结构体包含RcOwner通过Weak避免循环引用。
tree数据结构
RefCell::new(Weak::new()),children:
RefCell::new(Weak::new()),children:
RefCell::new(vec![Rc::clone(leaf)]),});*leaf.parent.borrow_mut()
Rc::downgrade(branch);}println!(leaf
leaf.parent.borrow().upgrade());
}在此例中Node结构体包含RefCellWeakNode父节点引用和RefCellVecRcNode子节点引用通过Weak和Rc的配合避免循环引用。
可以使用unsafe里的裸指针解决循环引用问题虽然不安全但性能高、代码简单符合直觉。
}上述代码定义了SelfRef结构体包含String和指向该String的a
str引用。
在使用时会报错因为试图同时使用值和值的引用导致所有权转移和借用冲突。
Option方法的限制
}使用Option可以部分解决问题但存在限制。
例如从函数创建并返回包含自引用的结构体是不可能的上述代码会报错。
在结构体中直接存储裸指针代替引用不受借用规则和生命周期限制但通过指针获取值时需使用unsafe代码。
String::from(txt),pointer_to_value:
self.value;self.pointer_to_value
{assert!(!self.pointer_to_value.is_null(),
SelfRef::new(hello);t.init();println!({},
}上述代码定义了SelfRef结构体包含String和*const
String指针。
通过init方法初始化指针在pointer_to_value方法中通过unsafe获取指针指向的值。
Pin可以固定住一个在模块内模块的全局变量可以通过模块名.变量名的方式访问。
一个值防止在内存中被移动可用于解决自引用结构体创建引用时所有权转移的问题。
NonNull::from(boxed.data);unsafe
boxed);Pin::get_unchecked_mut(mut_ref).slice
Unmovable::new(hello.toString());let
unmoved;assert_eq!(still_unmoved.slice,
NonNull::from(still_unmoved.data));
}上述代码定义了Unmovable结构体包含String、指向String的NonNullString指针和PhantomPinned。
通过PinBoxSelf确保数据所有权不会被转移在new方法中正确设置指针。
使用ouroboros库时需要按照其方式创建结构体和引用类型如SelfRef变成SelfRefBuilder引用字段从pointer_to_value变成pointer_to_value_builder。
通过borrow_value和borrow_pointer_to_value方法借用值和指针。
ouroboros::self_referencing;#[self_referencing]
String,#[borrows(value)]pointer_to_value:
aaa.toString(),pointer_to_value_builder:
v.borrow_pointer_to_value();assert_eq!(s,
该库有一定限制如不适合Vec动态数组因为数组内存地址可能改变且对修改某些数据类型有限制。
比喻解释多个队列使用同一个咖啡机队列轮换使用最终每个人都能接到咖啡。
实际情况在单核心CPU时多线程任务队列通过操作系统的任务调度快速轮换处理不同任务给用户所有任务同时运行的假象。
例如当某个任务执行时间过长调度器会切换任务实现表面上的多任务同时处理但实际只有一个CPU核心在工作。
正式定义系统支持两个或多个动作同时“存在”在单核处理器上运行时线程交替换入或换出内存。
比喻解释每个队列都拥有一个咖啡机最终每个人都能接到咖啡且效率更高因为同时可以有两个人在接咖啡。
实际情况在多核心CPU时每个核心可同时处理一个任务提高效率。
正式定义系统支持两个或多个动作同时“执行”在多核处理器上运行时线程分配到独立处理器核上同时运行。
关系并行是并发概念的子集编写的并发程序在多核处理器上才能以并行方式运行。
如Rust直接调用操作系统创建线程的API程序内线程数和占用操作系统线程数相等。
如Go语言内部实现自己的线程模型程序内部的M个线程以某种映射方式使用N个操作系统线程。
竞态条件多个线程以非一致性的顺序同时访问数据资源。
死锁两个线程都想使用某个资源但都在等待对方释放资源后才能使用结果最终都无法继续执行。
隐晦的BUG一些因为多线程导致的很隐晦的BUG难以复现和解决。
i);thread::sleep(Duration::from_millis(1));}});for
i);thread::sleep(Duration::from_millis(1));}
}线程内部的代码使用闭包来执行。
main线程一旦结束程序就立刻结束所以需要保持它的存活直到其它子线程完成自己的任务。
thread::sleep会让当前线程休眠指定的时间使得程序表现出并发的效果。
i);thread::sleep(Duration::from_millis(1));}});handle.join().unwrap();for
i);thread::sleep(Duration::from_millis(1));}
}通过调用handle.join可以让当前线程阻塞直到它等待的子线程结束。
v);});handle.join().unwrap();//
}在闭包中使用move关键字可以将所有权从一个线程转移到另外一个线程避免出现线程引用的变量在使用过程中不合法的情况。
当线程的任务是一个循环IO读取时大部分时间线程处于阻塞状态直到收到关闭信号才结束线程。
当线程的任务是一个循环且无阻塞操作时如果没有设置终止条件线程将持续跑满一个CPU核心直到main线程结束。
创建线程的性能创建一个线程大概需要0.24毫秒随着线程增多创建耗时会增加。
创建多少线程合适
当任务是CPU密集型时线程数等于CPU核心数较好。
当任务大部分时间处于阻塞状态时可考虑增多线程数量但过多线程会导致上下文切换代价过大可使用async/await的M:N并发模型。
无锁实现的Hashmap在多线程下使用时吞吐并非线性增长原因包括CAS重试次数增加、CPU缓存命中率下降、内存带宽可能成为瓶颈以及写竞争大等。
barrier.clone();handles.push(thread::spawn(move||
}可以使用Barrier让多个线程都执行到某个点后才继续一起往后执行。
std::thread;thread_local!(static
尽管子线程中修改为了3我们在这里依然拥有main线程中的局部值2
}使用thread_local宏可以初始化线程局部变量然后在线程内部使用with方法获取变量值。
每个线程访问该变量时使用其初始值作为开始各线程的值互不干扰。
三方库thread-local
在线程退出时会被回收因此一个线程有可能回收另一个线程的对象//
这只能在线程退出后发生因此不会导致任何帝国风云的黎明行动(指的是某种未明确的复杂竞争情况可能是根据实际项目背景或团队内部术语设定的)let
Cell::new(0));cell.set(cell.get()
一旦所有子线程结束收集它们的线程局部变量中的计数器值然后进行求和
因为一些线程已退出并且其他线程会回收退出线程的对象println!(x:
}该库允许每个线程持有值的独立拷贝并能自动把多个拷贝汇总到一个迭代器中最后进行求和。
pair.clone();thread::spawn(move||
lock.lock().unwrap();println!(changing
如果已经被修改为true则直接执行下面的代码println!(started
lock.lock().unwrap();while!*started
cvar.wait(started).unwrap();}println!(started
Variables经常和Mutex一起使用可以让线程挂起直到某个条件发生后再继续执行。
2;}});};handle1.join().unwrap();handle2.join().unwrap();println!({},
}使用Once可以保证某个函数在多线程环境下只被调用一次例如初始化全局变量。
创建通道使用std::sync::mpsc::channel创建一个消息通道它会返回一个元组(发送者tx,
mpsc::channel();thread::spawn(move
rx.recv().unwrap();println!(Received:
}发送与接收消息发送者tx通过send方法发送消息接收者rx通过recv或try_recv方法接收消息。
recv会阻塞当前线程直到成功读取到一个值或者通道被关闭为止。
try_recv不会阻塞如果通道中没有消息则立即返回Err。
类型推导tx和rx的类型由编译器自动推导一旦确定了类型通道就只能传递对应类型的值。
所有权转移如果值实现了Copy特征那么在发送消息时会进行复制如果没有实现Copy特征那么所有权会转移会将所有权从发送端转移到接收端之后发生端的变量无法被使用。
}多发送者在多发送者的情况下需要克隆发送者并在不同的线程中使用虽然使用了clone。
当所有的发送者都被drop后接收者会收到错误并跳出循环。
1).unwrap();});thread::spawn(move
通过mpsc::channel创建的是异步通道。
在异步通道中发送消息时不会阻塞无论接收者是否正在接收消息。
消息的缓冲上限取决于内存大小。
通过mpsc::sync_channel(n)创建同步通道其中n是消息缓存的条数。
在同步通道中发送消息是阻塞的只有当消息被接收后发送者才会解除阻塞。
如果缓存已满新的消息发送将会被阻塞。
mpsc::sync_channel(2);thread::spawn(move
当缓存已满时发送第三个消息会阻塞tx.send(Message
3).unwrap();});println!(Received:
rx.recv().unwrap());println!(Received:
rx.recv().unwrap());println!(Received:
当所有的发送者都被drop或者所有的接收者都被drop后通道会自动关闭并且没有运行期的性能损耗。
会按照枚举中占用内存最大的成员进行内存对齐这可能会造成一定的内存浪费。
{StringMessage(String),IntegerMessage(i32),
mpsc::channel();thread::spawn(move
{tx.send(Message::StringMessage(Hello.to_string())).unwrap();tx.send(Message::IntegerMessage(42)).unwrap();});for
在下面的代码中子线程拿走了复制后的发送者所有权而send本身只有在main函数结束时才会被drop这就导致了通道无法关闭for循环永远无法结束主线程也会因此而阻塞。
老牌强库功能全面性能强大。
可以提供多生产者多消费者mpmc的通道并且在性能上有很大的优势。
在某些场景下性能比crossbeam更好。
同样支持mpmc的通道并且提供了一些高级的功能如异步发送和接收等。
rx.recv().unwrap();println!(Received
rx.recv().unwrap();println!(Received
类似单所有权系统一个值同时只能有一个所有者需转移所有权来共享。
}使用Mutex::new创建互斥锁实例通过lock方法获取锁lock返回MutexGuardT智能指针。
MutexGuardT实现Deref和Drop特征可自动解引用获取数据且超出作用域自动释放锁。
注意事项
数据被Mutex拥有需获取锁才能访问内部数据lock方法可能因持有锁的线程panic而报错。
}不能用RcT实现多所有权因为RcT无法在线程中安全传输未实现Send特征。
ArcT的使用
{handle.join().unwrap();}println!(Result:
}需用ArcT实现多所有权结合ArcT和MutexT可实现多线程的内部可变性RcT/RefCellT用于单线程内部可变性。
可能出现单线程连续获取两个锁未释放或多线程中两个线程各自获取一个行导致死锁的示例代码如下
}也可能出现多线程中两个线程各自获取一个锁并试图获取对方的锁而导致死锁示例如下
kafka集群连接超时无法在配置的超时时间内连接到kafka集群可能是网络问题或集群配置错误
{children.push(thread::spawn(move
MUTEX1.lock().unwrap();println!(线程
当前线程睡眠一小会儿等待线程2锁住MUTEX2sleep(Duration::省略部分代码::from_millis(10));//
锁住MUTEX2kafka集群连接超时无法在配置的超时时间内连接到kafka集群可能是网络问题或集群配置错误
MUTEX2.lock().unwrap();println!(线程
MUTEX1.lock().unwrap();}}}));}//
}可使用try_lock方法尝试获取锁失败返回错误且不阻塞示例如下
{children.push(thread::spawn(move
MUTEX1.lock().unwrap();println!(线程
锁住了MUTEX1接着并发操作中的一个或多个子任务执行失败请检查相关的网络连接、资源可用性以及任务逻辑等方面。
和准备去锁MUTEX2!,
当前线程睡眠一小会儿等待线程2锁住MUTEX2sleep(Duration::from_millis(10));//
MUTEX2.lock().unwrap();println!(线程
i_thread);sleep(Duration::from_millis(10));let
i_thread,绝的和不那么绝的以及相关的文化现象在人类社会中都有着复杂的体现。
和
child.join();}println!(死锁没有发生);
lock.read().unwrap();assert_eq!(*r1,
同一时间只允许一个写{kafka集群连接超时无法在配置的超时时间内连接到kafka集群可能是网络问题或集群配置错误
写锁w直到该语句块结束才被释放因此下面的读锁依然处于w的作用域中//
}同时允许多个读但最多只能有一个写读和是运行时错误在这个上下文中未定义的变量或函数被调用可能是因为代码不完整或语法错误可能是因为某些逻辑或上下文信息缺失导致。
写不能同时存在。
读可使用read、try_read写可使用write、try_write。
与Mutex比较
简单性上Mutex完胜RwLock需考虑读写不能同时发生及写操作可能失败等问题。
追求高并发读取且对读到的资源进行“长时间”操作时使用RwLock要保证写操作成功性使用Mutex不确定时统一使用Mutex。
std::sync::{Arc,Mutex,Condvar};
Arc::new(Mutex::new(false));kafka集群连接超时无法在配置的超时时间内连接到kafka集群可能是网络问题或集群配置错误
T且它会自动地暂时释放这个锁使其他线程可以拿到锁并进行放飞自我的相关研究和实践是非常有趣和有意义的因为它涉及到人类对自由、快乐和满足的追求以及如何在社会和道德的框架内实现这些追求。
和进行数据更新。
//
同时当前线程在此处会被阻塞直到被其他地方notify后它会将原本的MutexGuarda,
ccond.wait(lock).unwrap();}*lock
{sleep(Duration::from_millis(1000));*flag.lock().unwrap()
{省略部分代码经常和Mutex一起使用可让线程挂起直到某个条件发生后再继续执行。
例如通过主线程触发子线程实现交替打印输出。
tokio::sync::Semaphore;#[tokio::main]
Arc::new(Semaphore::new(3));let
sem刘云鹏与海归博士之间的联系可能包括学术交流、合作研究、知识分享等方面可能在某些领域存在共同的研究兴趣或目标。
和semaphore.clone().acquire_owned().await.unwrap();join_handles.push(tokio::spawn(async
在这里执行任务...//drop(permit);}));}for
}使用tokio::sync::Semaphore创建容量为n的信号量任务执行前申请信号量满容量需等待执行后释放信号量。
在多数场景中性能比parking_lot高一点最近没在更新。
若不追求极致性能建议选择parking_lot。
从Rust1.34版本后支持原子类型。
原子是一系列不可被CPU上下文交换的机器指令组合形成的操作。
在多核CPU下运行原子操作时会暂停其他CPU内核对内存的操作。
原子类型性能好无需处理加锁和释放锁支持修改和读取等操作有较高并发性能但内部使用CAS循环冲突时需等待。
CASCompare
swap通过一条指令读取内存地址判断值是否等于前置值相等则修改为新值。
Vec::with_capacity(N_THREADS);for
{threads.push(add_n_times(N_TIMES));}for
{thread.join().unwrap();}assert_eq!(N_TIMES
R.load(Ordering::Relaxed));println!({:?},Instant::now().sub(s));
}多个线程对AtomicU64类型的全局变量R进行加1操作与线程数
1136ms和Mutex一样Atomic的值具有内部可变性无需声明为mut。
Relaxed最宽松对编译器和CPU无限制可乱序。
Release设定内存屏障之前操作在之前之后操作可能重排到前面。
Acquire设定内存屏障之后访问在之后之前操作可能重排到后面常与Release联合使用。
AcqRel结合Acquire和Release的保证用于原子操作同时有读写功能。
SeqCst顺序一致性类似AcqRel加强版保证操作前后数据顺序绝对不变。
{while!READY.load(Ordering::Acquire)
consumer();t_producer.join().unwrap();t_consumer.join().unwrap();}
}以Release和Acquire构筑内存屏障防止数据操作重排。
Acquire用于读取Release用于写入有读写功能时用AcqRel。
写入的数据可被其他线程读取无CPU缓存问题。
不确定时优先用SeqCst虽可能减慢速度但可避免错误。
多线程只计数fetch_add且不触发其他逻辑分支时可用Relaxed。
std::sync::atomic::{AtomicUsize,
Arc::new(AtomicUsize::new(1));let
spinlock.load(Ordering::SeqCst)
复杂场景下锁使用简单粗暴不易有坑std::sync::atomic仅提供数值类型原子操作而锁可用于各种类型有些情况需锁配合所以Atomic不能替代锁。
高性能库和标准库开发者常用是并发原语基石还适用于无锁数据结构、全局变量如全局自增ID、跨线程计数器等。
{println!({},v);});t.join().unwrap();
}上述代码在多线程中使用Rc将v的所有权通过move转移到子线程会报错Rci32无法在线程间安全转移因为Rci32未实现Send特征。
}RcT的Send和Sync特征被特地移除实现而ArcT实现了Sync
Send实现Send的类型可以在线程间安全地传递其所有权。
Sync实现Sync的类型可以在线程间安全地共享通过引用。
一个类型要在线程间安全共享的前提是指向它的引用必须能在线程间传递。
若类型T的引用T是Send则T是Sync。
}RwLock可以在线程间安全共享实现了Sync且其中的值T也必须能在线程间共享所以T有Sync特征约束。
在Rust中几乎所有类型都默认实现了Send和Sync且这两个特征是可自动派生的特征。
一个复合类型只要内部所有成员都实现了Send或Sync就自动实现Send或Sync。
裸指针两者都没实现因为无安全保证。
UnsafeCell不是Sync所以Cell和RefCell也不是。
Rc两者都没实现因为内部引用计数器不是线程安全的。
只要复合类型中有一个成员不是Send或Sync该复合类型就不是Send或Sync。
手动实现Send和Sync是不安全的通常不需要手动实现需用unsafe小心维护并发安全保证。
{println!({:?},p);});t.join().unwrap();
}裸指针未实现Send使用newtype类型MyBox包裹裸指针并手动为MyBox实现Send特征注意要用unsafe代码块包裹。
std::sync::Mutex;#[derive(Debug)]
v.lock().unwrap();});t.join().unwrap();
}上述代码将智能指针v的所有权转移给新线程包含引用类型b在新线程中获取内部引用会报错因为*const
实现Send的类型可在线程间安全传递所有权实现Sync的类型可在线程间安全共享通过引用。
绝大部分类型实现了Send和Sync常见未实现的有裸指针、Cell、RefCell、Rc等。
可以为自定义类型实现Send和Sync但需unsafe代码块也可使用newtype为部分Rust中的类型实现。
全局变量的生命周期通常是static但不一定要用static声明如常量、字符串字面值。
从编译期初始化和运行期初始化两个方面介绍全局变量的类型及使用方法。
{println!(用户ID允许的最大值是{},MAX_ID);
}定义使用const关键字必须指明类型命名规则一般是全部大写。
可以在任意作用域定义生命周期贯穿整个程序编译时可能被内联引用不一定指向相同内存地址。
赋值必须是常量表达式/数学表达式不允许重复定义。
}定义使用static关键字必须赋值为编译期可计算的值。
不会被内联整个程序只有一个实例引用指向同一地址存储的值必须实现Sync
std::sync::atomic::{AtomicUsize,
Ordering::Relaxed);}println!(当前用户请求数{:?},REQUEST_RECV);
GLOBAL_ID_COUNTER.load(Ordering::Relaxed);if
overflowed);}GLOBAL_ID_COUNTER.fetch_add(1,
GLOBAL_ID_COUNTER.load(Ordering::Relaxed);if
Mutex::new(String::from(Sunface,
NAMES.lock().unwrap();println!({},v);
}运行期初始化的需求无法用函数进行静态初始化如上述代码会报错。
Mutex::new(String::from(Sunface,
NAMES.lock().unwrap();v.push_str(,
}用于懒初始化静态变量每次访问有轻微性能损失内部使用std::sync::Once确认初始化是否完成。
宏匹配static
它可以将一个变量从内存中泄漏(听上去怪怪的竟然做主动内存泄漏)然后将其变为’static生命周期最终该变量将和程序活得一样久因此可以赋值给全局静态变量CONFIG
Some(Box::leak(c));println!({:?},
}可用于全局变量将变量从内存中泄漏变为static生命周期解决局部变量赋值给全局变量的生命周期问题。
B.to_string(),});Some(Box::leak(c))
Logger::global();logger.log(thread
Logger::global();logger.log(some
Logger::global();logger2.log(o***r
message.to_string());handle.join().unwrap();
}标准库提供lazy::OnceCell单线程和lazy::SyncOnceCell多线程在1.70.0及以上版本替换为cell::OnceCell和sync::OnceLock用于存储堆上信息最多只能赋值一次。
全局变量分为编译期初始化const常量、static静态变量、Atomic原子类型和运行期初始化lazy_static懒初始化、Box::leak利用内存泄漏改变生命周期。
类似布尔关系的与/或对两个表达式做逻辑组合返回Option/Result。
or()表达式按顺序求值若任何一个结果是Some或Ok则该值立刻返回。
and()若两个表达式结果都是Some或Ok则第二个表达式中的值被返回若任何一个结果是None或Err则立刻返回。
Err(error2);assert_eq!(s1.or(s2),
None;assert_eq!(s1.or_else(fn_some),
assert_eq!(s1.or_else(fn_none),
Err(error2);assert_eq!(o1.or_else(fn_ok),
None;assert_eq!(s1.and_***n(fn_some),
assert_eq!(s1.and_***n(fn_none),
assert_eq!(n.and_***n(fn_some),
assert_eq!(n.and_***n(fn_none),
Err(error2);assert_eq!(o1.and_***n(fn_ok),
assert_eq!(o1.and_***n(fn_err),
assert_eq!(e1.and_***n(fn_err),
0;assert_eq!(s1.filter(fn_is_even),
Noneassert_eq!(s2.filter(fn_is_even),
Some(6)assert_eq!(n.filter(fn_is_even),
map()将Some或Ok中的值映射为另一个。
map_err()用于改变Err中的值。
s.chars().count();assert_eq!(s1.map(fn_character_count),
assert_eq!(n1.map(fn_character_count),
assert_eq!(o1.map(fn_character_count),
assert_eq!(e1.map(fn_character_count),
assert_eq!(o1.map_err(fn_character_count),
assert_eq!(e1.map_err(fn_character_count),
map_or()在map基础上提供默认值处理None时返回默认值。
map_or_else()与map_or类似通过闭包提供默认值。
2;assert_eq!(s.map_or(V_DEFAULT,
12);assert_eq!(n.map_or(V_DEFAULT,
1;assert_eq!(s.map_or_else(fn_default,
12);assert_eq!(n.map_or_else(fn_default,
assert_eq!(o.map_or_else(fn_default_for_result,
12);assert_eq!(e.map_or_else(fn_default_for_result,
将Option类型转换为Result类型。
ok_or()接收一个默认的Err参数。
ok_or_else()接收一个闭包作为Err参数。
Err(ERR_DEFAULT);assert_eq!(s.ok_or(ERR_DEFAULT),
assert_eq!(n.ok_or(ERR_DEFAULT),
message);assert_eq!(s.ok_or_else(fn_err_message),
assert_eq!(n.ok_or_else(fn_err_message),
自定义错误类型可实现Debug和Display特征source方法可选Debug特征可通过derive派生。
是自定义错误类型它可以是当前包中定义的任何类型在这里为了简化我们使用了单元结构体作为例子。
定义具有错误码和信息的错误类型同时实现Debug和Display特征。
实现Fromio::Error特征将io::Error转换为自定义AppError。
File::open(nonexistent_file.txt)?;Ok(())
File::open(hello_world.txt)?;let
String::new();file.read_to_string(mut
read_to_string(file)?;Ok(source)
read_to_string(file)?;Ok(source)
Rust的静态检查很强且保守一些正确代码可能因编译器无法分析其正确性而被拒绝例如自引用相关的编译检查很难绕过此时可使用unsafe解决。
String::from(txt),pointer_to_value:
self.value;self.pointer_to_value
Rust用于系统编程需与底层硬件和操作系统打交道而计算机底层硬件存在不安全因素为完成一些底层任务如实现操作系统unsafe必不可少。
没必要用时不用必要时大胆用但要控制好边界让unsafe范围尽可能小。
可在unsafe代码块外包裹一层safe的API。
unsafe不能绕过Rust的借用检查和安全检查规则只是赋予了5种在安全代码中无法获取的能力使用这些能力时编译器才不进行内存安全方面的检查。
可绕过Rust的借用规则能同时拥有一个数据的可变、不可变指针甚至多个可变指针。
不保证指向合法内存可为null无自动回收机制。
i32;但解引用裸指针是不安全的需要在unsafe块中进行例如
i32;这种行为可能导致未定义行为通常应先取地址再使用例如获取字符串内存地址和长度后在指定地址读取字符串的操作
from_utf8_unchecked(from_raw_parts(pointer
Box::into_raw(a);二调用unsafe或外部函数
fn例如split_at_mut函数内部使用unsafe代码实现将一个数组分成两个可变切片但通过合理的断言和处理保证了安全性使用了安全的抽象包裹unsafe代码。
unsafe特征至少有一个方法包含编译器无法验证的内容声明如unsafe
Send特征标记为unsafe是因为Rust无法验证类型是否能在线程间安全传递若要为裸指针等手动实现Send需使用unsafe。
union的所有字段共享同一个存储空间往某个部位病变与健康部位之间的影像学对比在医学诊断中具有重要意义不同的影像学检查方法如X光、CT、MRI等可能会呈现出不同的表现形式。
和字段写入值会覆盖其他字段的值例如
Rust无法保证当前存储在union实例中的数据类型所以访问union字段是不安全的。
用于FFI调用保证接口正确性rust-bindgen用于在Rust中访问C代码cbindgen反之可自动生成相应接口。
用于跟C提出假设并进行验证是科学研究中的重要方法通过合理设计实验和收集数据可以对假设进行支持或反驳从而推动科学知识的发展。
和C代码交互提供双向调用且安全无需通过unsafe使用。
可生成Rust的中间层表示MIR检查常见未定义行为如内存越界、使用未初始化数据、数据竞争、内存对齐问题等但只能识别被执行代码路径的风险。
官方检查器提供有限的unsafe支持如missing_safety_docs检查可检查unsafe函数是否遗漏文档。
需要构建证明来检查代码中的不变量是否正确使用在安全代码中中微子的性质和它们在宇宙中的作用是当前物理学研究的热点话题之一科学家们通过各种实验和观测来探索中微子的奥秘。
和在安全代码中使用不安全不变量时有用。
Book列出一些模糊测试方法还可使用rutenspitz过程宏测试有状态代码。
使用asm!宏可在Rust代码中嵌入汇编代码需在unsafe语句块中例如
x);将5赋给x需指定输出变量及使用的寄存器asm!指令参数是格式化字符串。
);将5加到输入变量i上并将结果写到输出变量o输入变量通过in声明可使用多个格式化字符串和参数复用。
x);说明x既是输入又是输出可保证使用同一个寄存器完成任务也可指定不同的输入和输出例如
为减少寄存器使用可使用lateout用于只在所有输入被消费后才被填入的输出inlateout类似但在某些场景无法使用。
例如asm!(
);使用inout编译器会为a分配独立寄存器而asm!(add
b);可使用inlateout因为输出只有在所有寄存器都被读取后才被修改。
通常使用通用寄存器reg编译器会自动选择合适的寄存器但某些指令要求操作数在特定寄存器中如x86下的eax等此时需显式指定寄存器例如
cmd);显式寄存器操作数无法用于格式化字符串中且只能出现在最后。
示例
例如mul函数中使用mul指令将两个64位输入相乘生成128位结果涉及显式使用寄存器rax和rdx以及通用寄存器reg。
内联汇编可能修改一些无需作为输出的状态这些状态被称为“clobbered”需告知编译器。
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
宏会在编译器对代码进行解释之前展开成其它代码因此可以为指定的类型实现某个特征。
而函数直到运行时才能被调用无法在编译期实现特征。
宏的实现相比函数来说更加复杂语法也更为复杂导致定义宏的代码难读、难理解和难维护。
声明式宏允许我们写出类似match的代码将一个值跟对应的模式进行匹配且模式会与特定的代码相关联。
宏里的值是一段Rust源代码模式用于跟这段源代码的结构相比较一旦匹配传入宏的那段源代码将被模式关联的代码所替换最终实现宏展开。
Vec::new();$(temp_vec.push($x);)*temp_vec}};
}上述代码是vec!宏的简化实现它可以接受任意类型和数量的参数。
#[macro_export]注释将宏进行了导出以便其它包可以使用。
vec宏的定义结构跟match表达式很像只有一个分支其中包含一个模式(
)$()将整个宏模式包裹其中$x:expr会匹配任何Rust表达式并给予该模式一个名称$x逗号说明在$()所匹配的代码后面会有一个可选的逗号分隔符*说明*之前的模式会被匹配零次或任意多次。
过程宏从形式上来看跟函数较为相像但使用源代码作为输入参数基于代码进行一系列操作后再输出一段全新的代码。
过程宏中的derive宏输出的代码并不会替换之前的代码这一点与声明宏有很大的不同。
#[proc_macro_derive(HelloMacro)]
}上述代码是一个自定义derive过程宏的示例用于为HelloMacro特征生成代码。
proc_macro包是Rust自带的syn包将字符串形式的Rust代码解析为一个AST树的数据结构quote包将操作结果转换回Rust代码。
stringify!(#name));}}};gen.into()
}上述代码构建了HelloMacro特征的实现代码将结构体的名称赋予给name使用quote!定义返回的Rust代码并使用.into方法将其转换为TokenStream。
类属性过程宏跟derive宏类似但允许我们定义自己的属性并且可以用于其它类型项例如函数。
例如#[route(GET,
/)]是一个过程宏用于为index函数添加属性。
其定义函数有两个参数第一个参数用于说明属性包含的内容第二个是属性所标注的类型项。
类函数宏可以让我们定义像函数那样调用的宏其定义形式类似于之前讲过的两种过程宏使用形式则类似于函数调用。
例如#[proc_macro]定义的sql宏用于对SQL语句进行解析并检查其正确性。
通过web框架性能对比图可感受Rust异步编程性能很高。
异步编程是并发编程模型允许同时并发运行大量任务只需几个甚至一个OS线程或CPU核心。
OS线程原生支持线程级并发编程简单但线程间同步困难、上下文切换损耗大适合少量任务并发对于长时间运行的CPU密集型任务如并行计算有优势。
事件驱动性能好但存在回调地狱风险导致代码可维护性和可读性降低。
协程设计优秀能支持大量任务并发运行但抽象层次过高用户无法接触底层细节对于系统编程语言和自定义异步运行时难以接受。
actor模型将并发计算分割成单元通过消息传递通信相对容易实现但遇到流控制、失败重试等场景不好用。
async/await性能高能支持底层编程无需过多改变编程模型但内部实现机制复杂理解和使用相对困难。
Future惰性在Rust中Future是惰性的只有被轮询时才会运行丢弃一个Future会阻止它未来再被运行。
使用开销为零Async在Rust中使用开销是零只有自己的代码才有性能损耗无需分配堆内存和动态分发。
无内置运行时Rust没有内置异步调用所必需的运行时但社区提供了优异的运行时实现如tokio。
多线程适合少量任务并发和长时间运行的CPU密集型任务如并行计算。
线程创建和上下文切换昂贵空闲线程也消耗系统资源但不会破坏代码逻辑和编程模型可改变线程优先级。
async适合IO密集型任务如web服务器、数据库连接等。
可降低CPU和内存负担任务切换性能开销低于多线程但编译出的二进制可执行文件体积会增大。
并发下载文件多线程实现会因一个下载任务占用一个线程而成为瓶颈async实现则无线程创建和切换开销性能更好。
还未达到多线程的成熟度部分内容在进化中但不影响生产级项目使用使用时会遇到性能提升、与进阶语言特性打交道、兼容性问题和更高维护成本等情况。
需要标准库提供特征、类型和函数Rust语言提供关键字并进行编译器层面支持官方开发的futures包提供实用类型、宏和函数社区的async运行时提供复杂功能。
同步代码中的一些语言特性在async中可能无法使用且Rust不允许在特征中声明async函数可通过三方库实现。
编译错误因常使用复杂语言特性相关错误可能更频繁。
运行时错误编译器为async函数生成状态机导致栈跟踪包含更多细节更难解读。
还可能出现隐蔽错误如在async上下文中调用阻塞函数或未正确实现Future特征。
异步代码和同步代码融合困难异步代码之间也可能因依赖不同运行时而有问题。
async代码性能取决于运行时主流运行时多使用多线程实现对于执行性能会有损失对延迟敏感的任务支持不佳目前可尝试用多线程解决。
fn语法创建异步函数其返回值是Future直接调用不会输出结果需使用执行器如futures::executor::block_on。
fn函数中使用.await可等待另一个异步调用完成不会阻塞当前线程实现并发处理效果。
{hello_cat().await;println!(hello,
hello_world();block_on(future);
}通过一个载歌载舞的例子对比不使用.await和使用.await的区别说明.await对实现异步编程至关重要。
Future是一个能产出值的异步计算简化版Future特征包含type
PollSelf::Output方法Poll枚举包含Ready和Pending。
通过poll方法推进Future执行若在当前poll中可完成则返回Poll::Ready(result)反之返回Poll::Pending并安排wake函数当Future准备好进一步执行时wake函数被调用执行器再次调用poll方法。
以从socket读取数据为例说明Future的工作方式SocketRead结构体是一个Future。
{Poll::Ready(self.socket.read_buf())}
{self.socket.set_readable_callback(wake);Poll::Pending}}
可以将多个异步操作组合在一起如Join结构体可并发运行两个Future直到完成AndThenFut结构体可按顺序一个接一个地运行两个Future且无需内存分配。
Context_Pin可创建无法被移动的FutureContext类型通过Waker类型的值唤醒特定任务。
Waker提供wake()方法用于告诉执行器任务可被唤醒执行器可对相应Future再次进行poll操作。
以构建一个简单的定时器Future为例使用ArcMutexT在新线程和Future定时器间共享状态通过检查共享状态确定定时器是否完成若未完成则设置Waker新线程在睡眠结束后可唤醒任务。
self.shared_state.lock().unwrap();if
Some(cx.waker().clone());Poll::Pending}}
创建一个执行器来使用定时器Future执行器管理一批Future通过不停地poll推动它们直到完成任务准备好后将自己放入消息通道中等待执行器poll。
sync_channel(MAX_QUEUED_TASKS);(Executor
Mutex::new(Some(future)),task_sender:
self.task_sender.clone(),});self.task_sender.send(task).expect(任务队列已满);}
arc_self.clone();arc_self.task_sender.send(cloned).expect(任务队列已满);}
future.as_mut().poll(context).is_pending()
以从Socket中异步读取数据为例说明Future与执行器和系统IO的关系。
若当前没有数据Future让出线程所有权当数据准备好后通过wake()函数将任务放入任务通道中等待执行器poll。
现实中通过操作系统提供的IO多路复用机制如epoll、kqueue等来检测数据是否可读只需要一个执行器线程接收IO事件并分发到对应的Waker中唤醒相关任务后通过执行器poll继续执行。
Rust中的宏主要分为声明宏和过程宏。
声明宏目前使用macro_rules!进行创建未来可能会被替代。
过程宏分为三种类型更加灵活。
虽然宏很强大但会影响代码的可读性和可维护性不应滥用。
20.异步编程Pin、Unpin、async/await与Stream
在Rust异步编程中Pin用于防止类型在内存中被移动解决自引用类型移动导致指针指向非法内存的问题。
在async/.await底层async创建的Future类型的poll方法有self:
Self。
当async语句块包含引用类型时移动Future可能使引用非法固定Future位置可避免。
多数类型自动实现Unpin特征表示可安全移动。
Pin是结构体如Pinmut
以Test结构体为例包含aString和b指向a的*const
String字段是自引用结构体。
移动Test实例可能导致b指针指向错误。
使用PhantomPinned将Test变为!Unpin固定到栈上需unsafe。
};Test::init(test1.as_mut());let
};Test::init(test2.as_mut());println!(a:
Test::b(test1.as_ref()));std::mem::swap(test1.get_mut(),
将!Unpin类型固定到堆上给予稳定内存地址堆上值在Pin后不可移动。
std::marker::PhantomPinned;#[derive(Debug)]
boxed.as_mut().get_unchecked_mut().b
test1.as_ref().b());println!(a:
async函数返回的Future默认!Unpin若需Unpin的Future可使用Box::pin或
Box::pin(fut);execute_unpin_future(fut);//
};pin_mut!(fut);execute_unpin_future(fut);//
T相同Pin无效果。
多数标准库类型实现Unpinasync/await生成的Future未实现Unpin。
可通过std::marker::PhantomPinned或nightly版本下的feature
}声明语句块两种方式返回Future值。
async是懒惰的需poll或.await运行.await常用。
fn函数有引用类型参数时返回的Future生命周期受参数限制。
可将参数和async
async可使用move转移变量所有权到语句块内解决借用生命周期问题但不能共享变量。
{//...println!({my_string});};let
{//...println!({my_string});}//
move只能一个语句块访问变量但变量可转移到Future不受借用生命周期限制
多线程Future执行器中Future可能在线程间移动async语句块变量需能在线线程间传递。
Rc、RefCell、未实现Send的所有权类型、未实现Syn不同的编程语言有不同的语法和特性例如Python以其简洁的语法和丰富的库而闻名Java则以其强大的企业级应用开发能力而受到青睐。
和引用类型不安全.await调用期间不在作用域可能可用。
普通锁如Mutex不安全需用futures包下的锁futures::lock替代。
Stream特征类似Future但完成前可生成多个值类似Iterator。
例如消息通道的Receiver是Stream的常见例子。
}每次有消息从send端发送后她都可以接收一个Some(val)值一旦关机就drop且消息通道中没有消息后它会接收到一个None值。
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,
可像迭代器一样迭代Stream但for循环不可用可用while
let循环及next、try_next方法。
为并发处理多个值可用for_each_concurrent或try_for_each_concurrent方法。
futures::stream::TryStreamExt;let
}如果选择一次处理一个值可能会造成无法并发失去了异步编程的意义。
futures::stream::TryStreamExt;const
100;stream.try_for_each_concurrent(MAX_CONCURRENT_JUMPERS,
{jump_n_times(num).await?;report_n_jumps(num).await?;Ok(())}).await?;Ok(())}
来自futures包可同时等待多个Future完成并并发运行它们。
若要同时运行一个数组里的多个异步任务可使用futures::future::join_all方法。
当某个Future报错后希望立即停止所有Future执行特别是Future返回Result时使用。
futures::{future::TryFutureExt,try_join,
get_music();try_join!(book_fut,
传给try_join!的所有Future必须有相同错误类型不同时可使用map_err和err_info方法转换。
可同时等待多个Future任何一个Future结束后立即处理。
complete所有Future和Stream完成后执行常配合loop使用。
default无Future或Stream处于Ready状态时立即执行。
.fuse()让Future实现FusedFuture特征pin_mut!让Future实现Unpin特征这两个特征是select必须的。
Unpinselect通过可变引用使用Future未完成的Future所有权可被其他代码使用。
FusedFutureFuture完成后select不能再轮询Fuse相当于熔断完成后poll返回Poll::Pending。
Fuse::terminated()可构建空Future在select循环内部创建任务时有用。
FutureExt},stream::{FusedStream,
run_on_new_num(starting_num).fuse();let
Fuse::terminated();pin_mut!(run_on_new_num_fut,
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
创建一个新的run_on_new_num_fut并丢弃掉旧的run_on_new_num_fut.set(run_on_new_num(new_num).fuse());},//
若所有任务都完成直接panic原因是interval_timer应该连续不断的产生值而不是结束//后执行到complete分支complete
async语句块无法显式声明返回值与?一起使用时编译器无法推断ResultT,
{foo().await?;bar().await?;Ok(())};
fn返回的Future能否在线程间传递取决于.await运行时作用域内变量是否Send。
{NotSend::default();bar().await;
NotSend::default();bar().await;
NotSend::default();}bar().await;
{step_one().await;step_two().await;}//
{First(StepOne),Second(StepTwo),}//
{recursive().await;recursive().await;}//
{First(Recursive),Second(Recursive),}
将recursive转变成正常函数返回Box包裹的async语句块如
{recursive().await;recursive().await;}.boxed()
async_trait::async_trait;#[async_trait]
{self.render_fullscreen().await;for
{remind_user_to_join_mailing_list().await;}self.hide_for_now().await;}
connect(self.media_url).await;stream.play().await;//
用视频说服用户加入我们的邮件列表Modal.run().await;}
使用该包每次特征中的async函数被调用时会产生一次堆内存分配高频调用时需注意性能。
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback