共计 3585 个字符,预计需要花费 9 分钟才能阅读完成。
作者:谢敬伟,江湖人称“刀哥”,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();})
异步代码:逾越 await 问题
如上所述,个别地,咱们会将数据的值转移入线程,这样只须要做正确的 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