周刊(第19期):Rust并发安全相关的几个概念(上)
引言:本文介绍Rust并发安全相关的几个概念:Send
、Sync
、Arc
,Mutex
、RwLock
等之间的联系。这是其中的上篇,主要介绍Send
、Sync
这两个trait
。
Rust并发安全相关的几个概念(上)
Rust的所有权概念
在展开介绍并发相关的几个概念之前,有必要先了解一下Rust的所有权概念,Rust对值(value)的所有权有明确的限制:
- 一个值只能有一个owner。
- 可以同时存在同一个值的多个共享的非可变引用(immutable reference)。
- 但是只能存在一个值的可变引用(mutable reference)。
比如下面这段代码,user在创建线程之后,被移动(move)到两个不同的线程中:
fn main() {
let user = User { name: "drogus".to_string() };
let t1 = spawn(move || {
println!("Hello from the first thread {}", user.name);
});
let t2 = spawn(move || {
println!("Hello from the second thread {}", user.name);
});
t1.join().unwrap();
t2.join().unwrap();
}
由于一个值只能有一个owner
,所以编译器报错,报错信息如下:
error[E0382]: use of moved value: `user.name`
--> src/main.rs:15:20
|
11 | let t1 = spawn(move || {
| ------- value moved into closure here
12 | println!("Hello from the first thread {}", user.name);
| --------- variable moved due to use in closure
...
15 | let t2 = spawn(move || {
| ^^^^^^^ value used here after move
16 | println!("Hello from the second thread {}", user.name);
| --------- use occurs due to use in closure
|
= note: move occurs because `user.name` has type `String`, which does not implement the `Copy` trait
Send和Sync的约束作用
于是,如果一个类型会被多个线程所使用,是需要明确说明其共享属性的。Send
和Sync
这两个trait
作用就在于此,注意到这两个trait
都是std::marker
,实现这两个trait
并不需要对应实现什么方法,可以理解为这两个trait
是类型的约束
,编译器通过这些约束
在编译时对类型进行检查。到目前为止,暂时不展开对两个概念的理解,先来看看两者是如何在类型检查中起约束
作用的。
比如std::thread::spawn()
的定义如下:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
可以看到,对于spawn
传入的函数和返回的类型,都要求满足Send
这个约束。结合前面Send
的定义:
- 函数类型F需要满足
Send
约束:这是因为创建线程之后,需要把函数类型传入新创建的线程里,于是要求所有权能够在线程之间传递。 - 返回类型需要满足
Send
约束:这是因为创建线程之后,返回值也需要转移回去原先的线程。
有了对类型的约束,编译器就会在调用std::thread::spawn
函数时针对类型进行检查,比如下面这段代码:
#[derive(Debug)]
struct Foo {}
impl !Send for Foo {}
fn main() {
let foo = Foo {};
std::thread::spawn(move || {
dbg!(foo);
});
}
类型Foo
标记自己并不实现Send
这个trait
,于是在编译的时候报错了:
error[E0277]: `Foo` cannot be sent between threads safely
--> src/main.rs:7:5
|
7 | std::thread::spawn(move || {
| _____^^^^^^^^^^^^^^^^^^_-
| | |
| | `Foo` cannot be sent between threads safely
8 | | dbg!(foo);
9 | | });
| |_____- within this `[closure@src/main.rs:7:24: 9:6]`
|
= help: within `[closure@src/main.rs:7:24: 9:6]`, the trait `Send` is not implemented for `Foo`
= note: required because it appears within the type `[closure@src/main.rs:7:24: 9:6]`
note: required by a bound in `spawn`
如果把impl !Send for Foo {}
这一行去掉,代码就能编译通过了。
以上还有一个知识点:所有类型默认都是满足Send
、Sync
约束的,直到显示声明不满足这个约束,比如上面的impl !Send
就是这样一个显示声明。这就带来一个疑问:能不能跟编译器耍一些心思,明明某个类型就不满足这个约束,睁一只眼闭一只眼看看能不能在编译器那里蒙混过关?
答案是不能,编译器会检查这个类型中所有包含的成员,只有所有成员都满足这个约束,该类型才能算满足约束。可以在上面的基础上继续做实验,给Foo
结构体新增一个Rc
类型的成员:
#[derive(Debug)]
struct Foo {
rc: Option<std::rc::Rc<i32>>,
}
fn main() {
let foo = Foo { rc: None };
std::thread::spawn(move || {
dbg!(foo);
});
}
由于Rc
并不满足Send
约束(即显示声明了impl !Send
,见:impl-send),导致类型Foo
并不能蒙混过关满足Send
约束,编译上面代码时报错信息如下:
error[E0277]: `Rc<i32>` cannot be sent between threads safely
--> src/main.rs:8:5
|
8 | std::thread::spawn(move || {
| _____^^^^^^^^^^^^^^^^^^_-
| | |
| | `Rc<i32>` cannot be sent between threads safely
9 | | dbg!(foo);
10 | | });
| |_____- within this `[closure@src/main.rs:8:24: 10:6]`
|
= help: within `[closure@src/main.rs:8:24: 10:6]`, the trait `Send` is not implemented for `Rc<i32>`
= note: required because it appears within the type `Option<Rc<i32>>`
note: required because it appears within the type `Foo`
因此:一个类型要满足某个约束,当且仅当该类型下的所有成员都满足该约束
才行。
理解Send和Sync trait
继续回到Send
和Sync
这两个trait
中来,两者在rust官方文档中定义如下:
Send
:Types that can be transferred across thread boundaries。Sync
:Types for which it is safe to share references between threads。
上面的定义翻译过来:
Send
标记表明该类型的所有权可以在线程之间传递。Sync
标记表明该类型的引用可以安全的在多个线程之间被共享。
我发现上面的这个解释还是有点难理解了,可以换用更直白一点的方式来解释这两类约束:
-
Send
:- 满足
Send
约束的类型,能在多线程之间安全的排它使用(Exclusive access is thread-safe)。 - 满足
Send
约束的类型T
,表示T
和&mut T
(mut
表示能修改这个引用,甚至于删除即drop
这个数据)这两种类型的数据能在多个线程之间传递,说得直白些:能在多个线程之间move
值以及修改引用到的值。
- 满足
-
Sync
:- 满足
Sync
约束的类型,能在多线程之间安全的共享使用(Shared access is thread-safe)。 - 满足
Sync
约束的类型T
,只表示该类型能在多个线程中读共享,即:不能move
,也不能修改,仅仅只能通过引用&T
来读取这个值。
- 满足
-
有了上面的定义,可以知道:一个类型
T
的引用只有在满足Send
约束的条件下,类型T
才能满足Sync
约束(a type T is Sync if and only if &T is Send)。即:T: Sync ≡ &T: Send
。
对于那些基本的类型(primitive types)而言,比如i32
类型,大多是同时满足Send
和Sync
这两个约束的,因为这些类型的共享引用(&)既能在多个多个线程中使用,同时也能在多个线程中被修改(&mut )。
了解了Send
和Sync
这两类约束,就可以接着看在并发安全中的运用了,这是下一篇的内容。
参考资料
- Arc and Mutex in Rust | It’s all about the bit
- Sync in std::marker - Rust
- Send in std::marker - Rust
- Send and Sync - The Rustonomicon
- rust - Understanding the Send trait - Stack Overflow
- Understanding Rust Thread Safety
- An unsafe tour of Rust’s Send and Sync | nyanpasu64’s blog
- Rust: A unique perspective