当前位置: 首页 > 后端技术 > Python

再谈SendandSync-Rust学习笔记

时间:2023-03-26 17:06:50 Python

作者:谢经纬,人称“刀哥”,20年IT从业者,数据通信网络专家,电信网络架构师,现任Netwarps开发总监。刀哥在操作系统、网络编程、高并发、高吞吐量、高可用等领域有多年的实践经验,对网络和编程方面的新技术有着浓厚的兴趣。Send和Sync可能是Rust多线程和异步代码中遇到的最常见的约束。这两个约束的起源在之前讨论多线程的文章中介绍过。但是,在实际编写比较复杂的代码时,往往会遇到编译器的各种不兼容问题。这里我就以同事遇到的一个问题为例,再次讲讲SendandSync的故事。Send/Sync的概念在基本场景的C/C++中是不存在的。数据对象可以在多个线程中任意访问,但需要程序员保证线程安全,也就是所谓的“加锁”。在Rust中,由于所有权的设计,一个对象不能直接分成两份或多份,每个线程都有一份。一般如果一条数据只被子线程使用,我们会将数据的值传递给线程,这也是Send的基本含义。因此,Rust代码经常看到数据clone(),然后移动到线程:letb=aa.clone();thread::spawn(move||{b...})如果数据需要在多个Thread共享,情况会比较复杂。我们一般不会直接在线程中使用外部环境变量引用。原因很简单,生命周期的问题。线程的关闭需要'static,会和借用的外部环境变量的生命周期发生冲突。错误代码如下:letbb=AA::new(8);thread::spawn(||{letcc=&bb;//closuremayoutlivethecurrentfunction,butitborrows`bb`,whichisowned通过当前函数});包裹一个Arc可以解决这个问题,而Arc正好用来管理生命周期。改进后的代码如下:letb=Arc::new(aa);letb1=b.clone();thread::spawn(move||{b1...})Arc提供了共享不可变引用的能力,即数据是只读的。如果我们需要访问变量引用进行多线程访问共享数据,即读写数据,那么我们还需要在原始数据上包裹Mutex,类似于RefCell,提供内部可变性,这样我们就可以获取内部数据,修改数据。当然,这需要通过Mutex::lock()来完成。letb=Arc::new(Mutex::new(aa));letb1=b.clone();thread::spawn(move||{letb=b1.lock();...})为什么不能直接用RefCell来完成这个功能吗?这是因为RefCell不支持Sync,无法加载到Arc中。注意Arc的约束:unsafeimplSendforArc{}如果Arc为Send,则条件为T:Send+Sync。RefCell不满足Sync,所以Arc>不满足Send,无法传递给线程。错误代码如下:letb=Arc::new(RefCell::new(aa));letb1=b.clone();thread::spawn(move||{^^^^^^^^^^^^^^`std::cell::RefCell>`无法在线程之间安全共享letx=b1.borrow_mut();})异步代码:跨越await如上所述,一般来说,我们将数据的值传递到线程中,所以只需要正确的Send和Sync标签,非常直观易懂。典型代码如下:fntest1(t:T){letb=Arc::new(t);让bb=b.clone();thread::spawn(move||{letcc=&bb;});}根据上面的分析,不难推导出条件的来龙去脉T:Send+Sync+'static:Closure:Send+'static?Arc:发送+'static?T:发送+Sync+'static。但是在异步协程代码中有一个普遍的情况,推导过程比较隐蔽,值得一谈。考虑以下代码:structAA(T);implAA{asyncfnrun_self(self){}asyncfnrun(&self){}asyncfnrun_mut(&mutself){}}fntest2(mutaa:AA){letha=async_std::task::spawn(asyncmove{aa.run_self().await;});}在test2中,限制T:发送+'静态的,合理的。asyncfn生成的GenFuture需要Send+'static,所以捕获到放置在GenFuture匿名结构中的AA也必须满足Send+'static,然后要求AA泛型参数也满足Send+'static。但是,如果以类似的方式调用AA::run()方法,则编译失败,编译器提示GenFuture不满足Send。代码如下:fntest2(mutaa:AA){letha=async_std::task::spawn(asyncmove{^^^^^^^^^^^^^^^^^^^^^^^`test2`返回的future不是`Send`aa.run().await;});}原因是AA::run()方法的签名是&self,所以run()是通过aa的不可变借用&AA调用的。而run()是执行await的异步方法,也就是所谓的&aa交叉await,所以GenFuture匿名结构除了生成aa之外还需要生成&aa,原理图代码如下:struct{aa:AAaa_ref:&AA}前面讨论过,生成的GenFuture需要满足Send,所以AA和&AA都需要满足Send。而&AA满足Send,也就是说AA满足Sync。这就是各种Rust教程中提到的那句话的真正含义:对于任何类型T,如果&T是Send,则T是Sync。将之前的错误代码修改为如下形式,添加Sync标签,编译通过。fntest2(mutaa:AA){letha=async_std::task::spawn(asyncmove{aa.run().await;});}此外,值得指出的是,在上面的代码中调用AA::run_mut(&mutself)不需要同步标志:fntest2(mutaa:AA){letha=async_std::task::spawn(asyncmove{aa.run_mut().await;});}这是因为&mutself不需要T:Sync。看下面标准库中的Sync定义代码理解:modimpls{#[stable(feature="rust1",since="1.0.0")]unsafeimplSendfor&T{}#[stable(feature="rust1",since="1.0.0")]unsafeimplSendfor&mutT{}}可以看到&T:Send需要T:Sync,&mutT是T:发送。总结总而言之,Send约束是由thread::spawn()或task::spawn()在其根部引入的,因为这两个方法的闭包参数必须满足Send。此外,在共享数据需要T:Send+Sync时使用Arc。共享可写数据,需要Arc>,此时T:Send就足够了,不再需要Sync。关于异步代码中的Send/Sync与同步多线程代码没有区别。只是因为GenFuture的特殊性,await中的变量必须是T:Send。这时候需要注意通过T调用异步方法的签名,如果是&self,必须满足T:Send+Sync。最后,一点经验分享:Send/Sync的道理并不复杂。更多的时候是因为代码比较深,调用关系复杂,导致编译器的错误提示难以理解。在某些特定情况下,编译器仍然可能给出完全错误的修正建议。这时候就需要慎重考虑,追根溯源,找到问题的本质。您不能完全依赖编译器提示。深圳市网华科技有限公司(Netwarps),专注于互联网安全存储领域的技术研发与应用,是一家先进的安全存储基础设施提供商。其主要产品包括去中心化文件系统(DFS)、企业联盟链平台(EAC)、区块链操作系统(BOS)。微信公众号:网华