做者:谢敬伟,江湖人称“刀哥”,20年IT老兵,数据通讯网络专家,电信网络架构师,目前任Netwarps开发总监。刀哥在操做系统、网络编程、高并发、高吞吐、高可用性等领域有多年的实践经验,并对网络及编程等方面的新技术有浓厚的兴趣。程序员
Send 与 Sync 多是Rust多线程以及异步代码种最多见到的约束。在前面一篇讨论多线程的文章中介绍过这两个约束的由来。可是,真正书写比较复杂的代码时,仍是会常常遇到编译器的各类不配合。这里借用个人同事遇到的一个问题再次举例谈一谈 Send 与 Sync 的故事。编程
C/C++中不存在Send/Sync的概念,数据对象能够任意在多线程中访问,只不过须要程序员保证线程安全,也就是所谓“加锁”。而在Rust中,因为全部权的设计,不能直接将一个对象分红两份或多份,每一个线程都放一份。通常地,若是一份数据仅仅子线程使用,咱们会将数据的值转移至线程中,这也是Send的基础含义。所以,Rust代码常常会看到将数据clone(),而后move到线程中:安全
let b = aa.clone(); thread::spawn(move || { b... })
假如,数据须要在多线程共享,状况会复杂一些。咱们通常不会在线程中直接使用外部环境变量引用。缘由很简单,生命周期的问题。线程的闭包要求‘static,这会与被借用的外部环境变量的生命周期冲突,错误代码以下:微信
let bb = AA::new(8); thread::spawn( || { let cc = &bb; //closure may outlive the current function, but it borrows `bb`, which is owned by the current function });
包裹一个Arc能够解决这个问题,Arc刚好就是用来管理生命周期的,改进后的代码以下:网络
let b = Arc::new(aa); let b1 = b.clone(); thread::spawn(move || { b1... })
Arc提供了共享不可变引用的功能,也就是说,数据是只读的。若是咱们须要访问多线程访问共享数据的可变引用,即读写数据,那么还须要在原始数据上先包裹Mutex<T>
,相似于RefCell<T>
,提供内部可变性,所以咱们能够获取内部数据的&mut,修改数据。固然,这须要经过Mutex::lock() 来操做。多线程
let b = Arc::new(Mutex::new(aa)); let b1 = b.clone(); thread::spawn(move || { let b = b1.lock(); ... })
为何不能直接使用RefCell完成这个功能?这是由于RefCell不支持 Sync,没办法装入Arc。注意Arc的约束:闭包
unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}
若 Arc<T>
是Send,条件是 T:Send+Sync。RefCell不知足 Sync,所以 Arc<RefCell<>> 不知足Send,没法转移至线程中。错误代码以下:架构
let b = Arc::new(RefCell::new(aa)); let b1 = b.clone(); thread::spawn(move || { ^^^^^^^^^^^^^ `std::cell::RefCell<AA<T>>` cannot be shared between threads safely let x = b1.borrow_mut(); })
如上所述,通常地,咱们会将数据的值转移入线程,这样只须要作正确的 Send和Sync 标记便可,很直观,容易理解。典型的代码以下:并发
fn test1<T: Send + Sync + 'static>(t: T) { let b = Arc::new(t); let bb = b.clone(); thread::spawn( move|| { let cc = &bb; }); }
根据上面的分析,不难推导出条件 T: Send + Sync + 'static 的前因后果:Closure: Send + 'static ⇒ Arc<T>
: Send + ’static ⇒ T: Send + Sync + 'static。异步
然而,在异步协程代码中有一种常见状况,推导过程则显得比较隐蔽,值得说道说道。考察如下代码:
struct AA<T>(T); impl<T> AA<T> { async fn run_self(self) {} async fn run(&self) {} async fn run_mut(&mut self) {} } fn test2<T: Send + 'static>(mut aa: AA<T>) { let ha = async_std::task::spawn(async move { aa.run_self().await; }); }
test2 中,限定 T: Send + ‘static,合情合理。async fn 生成的 GenFuture 要求 Send + ‘static,所以被捕获置入 GenFuture 匿名结构中的 AA 也必须知足 Send + ‘static,进而要求AA 泛型参数也知足Send + ‘static。
然而,相似的方式调用 AA::run() 方法,编译失败,编译器提示 GenFuture 不知足 Send。代码以下:
fn test2<T: Send + 'static>(mut aa: AA<T>) { let ha = async_std::task::spawn(async move { ^^^^^^^^^^^^^^^^^^^^^^ future returned by `test2` is not `Send` aa.run().await; }); }
缘由在于,AA::run()方法的签名是 &self,因此run()是经过 aa 的不可变借用 &AA 来调用。而run()又是一个异步方法,执行了await,也就是所谓的&aa 跨越了 await,故而要求GenFuture匿名结构除了生成aa以外,还须要生成 &aa,示意代码以下:
struct { aa: AA aa_ref: &AA }
正如以前探讨过,生成的 GenFuture须要知足 Send,所以 AA 以及 &AA 都须要知足 Send。而&AA知足 Send,则意味着 AA 知足 Sync。这也就是各类 Rust教程中都会提到的那句话的真正含义:
对于任意类型 T,若是 &T是 Send ,T 就是 Sync 的
以前出错的代码修改成以下形式,增长 Sync标记,编译经过。
fn test2<T: Send + Sync + 'static>(mut aa: AA<T>) { let ha = async_std::task::spawn(async move { aa.run().await; }); }
另外,值得指出的是上述代码中调用 AA::run_mut(&mut self) 不须要 Sync 标记:
fn test2<T: Send + 'static>(mut aa: AA<T>) { let ha = async_std::task::spawn(async move { aa.run_mut().await; }); }
这是由于 &mut self 并不要求 T: Sync。参见如下标准库中关于Sync定义代码就明白了:
mod impls { #[stable(feature = "rust1", since = "1.0.0")] unsafe impl<T: Sync + ?Sized> Send for &T {} #[stable(feature = "rust1", since = "1.0.0")] unsafe impl<T: Send + ?Sized> Send for &mut T {} }
能够看到,&T: Send 要求 T: Sync,而 &mut T 则 T: Send 便可。
总而言之,Send约束在根源上是由 thread::spawn() 或是 task::spawn() 引入的,由于两个方法的闭包参数必须知足 Send。此外,在须要共享数据时使用Arc<T>
会要求 T: Send + Sync。而共享可写数据,须要Arc<Mutex<T>>
,此时 T: Send 便可,再也不要求Sync。
异步代码中关于 Send/Sync 与同步多线程代码没有不一样。只是由于GenFuture 的特别之处使得跨越 await 的变量必须是 T: Send,此时须要注意经过 T 调用异步方法的签名,若是为 &self,则必须知足 T:Send + Sync。
最后,一点经验分享:关于 Send/Sync 的道理并不复杂,更多时候是由于代码中层次比较深,调用关系复杂,致使编译器的错误提示很难看懂,某些特定场合编译器可能还会给出彻底错误的修正建议,这时候须要仔细斟酌,追根溯源,找到问题的本质,不能彻底依靠编译器提示。
深圳星链网科科技有限公司(Netwarps),专一于互联网安全存储领域技术的研发与应用,是先进的安全存储基础设施提供商,主要产品有去中心化文件系统(DFS)、企业联盟链平台(EAC)、区块链操做系统(BOS)。微信公众号:Netwarps