Send && Sync In Rust
date
slug
tags
summary
type
status
注意:该文章的理解都只是个人理解和推理,欢迎指出错误
Why Send && Sync
Send
和Sync
是Rust语言中用于确保并发安全的两个marker特征(marker trait),这意味着Send和Sync本身不提供任何额外功能,只是用于标记。Rust标准库中提供的并发类型会被标记好表示该工具能够用于并发场景,而对于用户自己的实现,当其确保其能够用于并发场景下(这意味着用户需要自己实现互斥等并发安全的操作),可以通过unsafe进行标记。Rust是如何保证并发安全呢?
Rust通过类型系统进行保证,具体来说,实现
Send
和Sync
trait的类型表示能够在并发场景下被安全的使用。因此,在涉及到并发场景的函数中,可以通过trait bound来限制传入的参数必须实现以上两个trait,保证其能够安全访问。例如,
thread::spawn
函数用来创建新线程并执行某个函数,它要求传递进来的函数的类型必须实现Send
特征。如下面,如果传入的函数中使用了没有实现Send trait的类型,则编译器会报错
本文将从定义,再到具体例子,去探究:为什么Send和Sync可以保证安全性?
定义
- Send的定义
Types that can be transferred across thread boundaries.
Send
特征的定义是,任何被标记为Send
的类型都可以安全地传递到另外一个线程。在Rust中,将一个值传递到另外一个线程这件事,本质是通过将closure捕获值,然后将closure传递到spawn函数完成的。比如上面的例子,在使用
thread::spawn
函数创建新线程时,该函数要求传入的闭包(或函数)必须满足Send
特征。这意味着,如果你想要在这个闭包中使用某个变量,那么这个变量的类型也必须实现Send
特征。这样的要求确保了只有实现了Send
特征的类型(即,可以被安全地传递到另外一个线程的类型)才可以被传递到其他线程。- Sync的定义通常是:
&T
isSend
if and only ifT
isSync
对于这句话的理解,我觉得最直观的方式是直接看实现:
对于实现了Sync的type,会为其引用类型自动实现Send。因此,我认为Sync的本质是对其引用类型实现Send,如果我们将引用看作一种类型,对于Sync如何保证安全性的讨论其实就等价于Send如何保证安全性的讨论。
安全性保证
前提:以下的分析将不可变引用,可变引用以及类型本身视为三种类型,即T, &T, &mut T是三种类型。
对于并发安全问题,本质是由于一块可变内存被共享,导致内存可能被同时修改,发生了一些在线性执行下不会发生的问题。
将Rust中类型做一个简单归类,分别讨论他们在并发场景下的安全性:
- 可变,且共享:
&RefCell<Vec<i32>>
- 不可变,且共享:
&Vec<i32>
- 可变,不共享:
&mut Vec<i32>
- 不可变,不共享:
Vec<i32>
这里的共享指的是,当前实例化的值同时也可以被其他值访问到。
对于2,3,4类,由于其不会涉及到并发修改,因此都是并发安全的,下面的程序进行2,3,4的行为,结果是可以通过编译的。
从Vec的定义可以看到,以上的类型确实实现了Send(Sync表示&Vec实现了Send
而对于1,可变且共享意味着该值会存在并发访问,因此对于这种类型的并发安全性,一般有两种可能
- !Send,防止该类型被用于并发场景
- Send,对于访问操作需要实现互斥操作
几个具体的例子:
- RefCell<Vec<i32>>是Send&&!Sync。
- RefCell:RefCell的值本身可以被传递到另外一个线程,因为RefCell以及Vec<i32>都是对持有的内存进行独占。因此,RefCell本身属于不共享,因此标记了Send,可以被安全的传递到另外一个线程。
- &RefCell:&RefCell可以用于获取其内部的mutable reference,且没有做任何互斥保护。因此&RefCell属于可变可共享,由于RefCell本身没有做互斥保护,因此需要标记为!Send,即RefCell标记为!Sync。
- Mutex和RefCell一样,可以通过其引用获取到内部数据的mutable reference。但是,Mutex对这个获取的操作做了互斥保护,可以在运行时保证同一时间只有一个线程会获取mutable reference,因此它是Sync的。
- Rc是一个带引用计数的智能指针,当Rc被drop时,会减少其引用计数,若为0,则将对象释放掉。但是Rc对其引用计数的操作不是原子的,因此Rc只能用于单线程,Rc是Not Send和Not Sync。
- 首先Rc会共享引用计数,且在Drop时会操作引用计数。因此Rc本身属于可变可共享,由于Drop操作引用计数的方法不是互斥的,因此Rc是Not Send的。
- &Rc可以通过Clone来增加引用计数,这意味&Rc同样是可变可共享,同理,其也是!Send,即Rc是!Sync。
Arc
Arc是一个比较特殊例子,因此单独拎出来。
A thread-safe reference-counting pointer. The typeArc<T>
provides shared ownership of a value of typeT
简单说明下Arc:
Arc是一种延长引用生命周期的方式(本质是共享指针,使用了引用计数),在下面的例子中,1.会报错,因为线程运行中会使用来自five的引用,但其无法确保five的生命周期会一直持续到线程结束,因此Rust注明spawn必须使用生命周期为全局的数据。通过Arc,可以使得five的生命周期会一直持续到所有Arc Drop为止。
从上面的例子可以看到,其实可以把Arc的本质看作引用,Arc<T>看作&T,意味着Arc是可共享的。
因此,而由于Arc内部的ref count都是原子实现的,因此Arc本身的数据是并发安全的,因此,可以推断:Arc具体是不是并发安全,取决&T是不是并发安全的。
可以看到Arc对Send和Sync的实现符合推断:
Send for Async block && Closure
关于AsyncBlock和Closure是否为Send的规则,可以参考:
Async Block
async block其实是状态机,状态机需要存储await之间需要的状态,因此其是否为Send的规则为:当async block在await时持有一个非Send的变量,则不满足Send,(因为该变量需要保存在状态机中)。
接下来通过例子说明,如case1,由于value没有跨越await point,因此第一个case1可以通过;如case2,由于value跨越await point,因此第二个case2无法通过。
此外,若async block捕捉了外部非Send的变量,由于该变量需要记录为状态机的初始状态,因此此时也不满足Send。
如下的case,async block捕获了外部的value,因此不满足Send
Closure
closure可以视为没有await point的async block,因此Send主要取决于捕获的变量。
如下的case,closure捕获了外部!Send的变量,因此其为!Send。
总结
到了这里,我们梳理了Send和Sync是如何保证安全性的:
- 通过类型系统,Send和Sync只是一种标记
- 会出现并发安全的类型在于可变可共享
- 对于可变可共享的类型,如果内部实现了互斥机制,会被标志为Send,如果没有,就通过标记!Send防止其被并发使用
Ref
Loading...