Send && Sync In Rust
注意:该文章的理解都只是个人理解和推理,欢迎指出错误
Why Send && Sync
**Send
和Sync
**是Rust语言中用于确保并发安全的两个marker特征(marker trait),这意味着Send和Sync本身不提供任何额外功能,只是用于标记。Rust标准库中提供的并发类型会被标记好表示该工具能够用于并发场景,而对于用户自己的实现,当其确保其能够用于并发场景下(这意味着用户需要自己实现互斥等并发安全的操作),可以通过unsafe进行标记。
Rust是如何保证并发安全呢?
Rust通过类型系统进行保证,具体来说,实现**Send
和Sync
** trait的类型表示能够在并发场景下被安全的使用。因此,在涉及到并发场景的函数中,可以通过trait bound来限制传入的参数必须实现以上两个trait,保证其能够安全访问。
例如,**thread::spawn
函数用来创建新线程并执行某个函数,它要求传递进来的函数的类型必须实现Send
**特征。
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
如下面,如果传入的函数中使用了没有实现Send trait的类型,则编译器会报错
let pointer = Rc::new(1);
thread::spawn(move || {
println!("{}",pointer)
});
error[E0277]: `Rc<i32>` cannot be sent between threads safely
--> src/main.rs:7:19
|
7 | thread::spawn(move || {
| ------------- ^------
| | |
| _____|_____________within this `{closure@src/main.rs:7:19: 7:26}`
| | |
| | required by a bound introduced by this call
8 | | println!("{}",pointer)
9 | | });
| |_____^ `Rc<i32>` cannot be sent between threads safely
|
= help: within `{closure@src/main.rs:7:19: 7:26}`, the trait `Send` is not implemented for `Rc<i32>`
note: required because it's used within this closure
本文将从定义,再到具体例子,去探究:为什么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
对于这句话的理解,我觉得最直观的方式是直接看实现:
unsafe impl<T: Sync + ?Sized> Send for &T {}
对于实现了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的行为,结果是可以通过编译的。
fn main() {
use std::thread;
use std::rc::Rc;
let mut v = vec![1];
// 2: Move &Vec<T> to another thread
// 3: Move &mut Vec<T> to another thread
thread::scope(|s| {
s.spawn(||{
(&mut v).push(1);
let _ = (&v).len();
});
});
println!("after: {:?}",v);
// 4: Move Vec<T> to another thread
let handle = thread::spawn(move || {
v.push(2);
});
handle.join();
}
从Vec的定义可以看到,以上的类型确实实现了Send(Sync表示&Vec实现了Send
而对于1,可变且共享意味着该值会存在并发访问,因此对于这种类型的并发安全性,一般有两种可能
- !Send,防止该类型被用于并发场景
- Send,对于访问操作需要实现互斥操作
几个具体的例子:
- RefCell<Vec
>是Send&&!Sync。 - RefCell:RefCell的值本身可以被传递到另外一个线程,因为RefCell以及Vec
都是对持有的内存进行独占。因此,RefCell本身属于不共享,因此标记了Send,可以被安全的传递到另外一个线程。 - &RefCell:&RefCell可以用于获取其内部的mutable reference,且没有做任何互斥保护。因此&RefCell属于可变可共享,由于RefCell本身没有做互斥保护,因此需要标记为!Send,即RefCell标记为!Sync。
- RefCell:RefCell的值本身可以被传递到另外一个线程,因为RefCell以及Vec
- 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为止。
fn main() {
use std::sync::Arc;
use std::thread;
// 1. 报错
let five = 5;
thread::spawn(|| {
println!("{}", &five);
});
// 2. 成功
let five = Arc::new(5);
thread::spawn(move || {
println!("{}", &five);
});
}
从上面的例子可以看到,其实可以把Arc的本质看作引用,Arc
因此,而由于Arc内部的ref count都是原子实现的,因此Arc本身的数据是并发安全的,因此,可以推断:Arc具体是不是并发安全,取决&T是不是并发安全的。
可以看到Arc对Send和Sync的实现符合推断:
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Sync + Send, A: Allocator + Send> Send for Arc<T, A> {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Sync + Send, A: Allocator + Sync> Sync for Arc<T, A> {}
Send for Async block && Closure
关于AsyncBlock和Closure是否为Send的规则,可以参考:
- https://rust-lang.github.io/async-book/07_workarounds/03_send_approximation.html
- https://doc.rust-lang.org/reference/types/closure.html#other-traits
Async Block
async block其实是状态机,状态机需要存储await之间需要的状态,因此其是否为Send的规则为:当async block在await时持有一个非Send的变量,则不满足Send,(因为该变量需要保存在状态机中)。
接下来通过例子说明,如case1,由于value没有跨越await point,因此第一个case1可以通过;如case2,由于value跨越await point,因此第二个case2无法通过。
// Pass!
async fn case1() {
let t = async move {
let value = Rc::new(RefCell::new(0));
println!("{}", value.borrow());
};
require_send(t);
}
// Fail!
async fn case2() {
let t = async move {
let value = Rc::new(RefCell::new(0));
async {}.await;
println!("{}", value.borrow());
};
require_send(t);
}
此外,若async block捕捉了外部非Send的变量,由于该变量需要记录为状态机的初始状态,因此此时也不满足Send。
如下的case,async block捕获了外部的value,因此不满足Send
async fn case() {
let value = Rc::new(RefCell::new(0));
let t = async move {
let value = value;
println!("{}", value.borrow());
};
require_send(t);
}
Closure
closure可以视为没有await point的async block,因此Send主要取决于捕获的变量。
如下的case,closure捕获了外部!Send的变量,因此其为!Send。
let value = Rc::new(RefCell::new(0));
let t = move || {
println!("{}", value.borrow());
};
require_send(t);
总结
到了这里,我们梳理了Send和Sync是如何保证安全性的:
- 通过类型系统,Send和Sync只是一种标记
- 会出现并发安全的类型在于可变可共享
- 对于可变可共享的类型,如果内部实现了互斥机制,会被标志为Send,如果没有,就通过标记!Send防止其被并发使用