Rust 并发安全相关的几个概念(上)
引言
本文介绍一下 Rust 并发安全相关的几个概念:Send、Sync、Arc,Mutex、RwLock 等之间的联系。这是其中的上篇,主要介绍 Send、Sync 这两个trait。
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:🧵: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:🧵: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[1]),导致类型 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
继续回到 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[2]
- Sync in std::marker - Rust[3]
- Send in std::marker - Rust[4]
- Send and Sync - The Rustonomicon[5]
- rust - Understanding the Send trait - Stack Overflow[6]
- Understanding Rust Thread Safety[7]
- An unsafe tour of Rust’s Send and Sync | nyanpasu64’s blog[8]
- Rust: A unique perspective[9]
关于 Databend
Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式数仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。
-
Databend 文档:https://databend.rs/
-
Wechat:Databend
文章首发于公众号:Databend
https://doc.rust-lang.org/std/rc/struct.Rc.html#impl-Send ↩︎
https://stackoverflow.com/questions/59428096/understanding-the-send-trait ↩︎
https://nyanpasu64.github.io/blog/an-unsafe-tour-of-rust-s-send-and-sync/#example-passing-mut-t-send-between-threads ↩︎
https://limpet.net/mbrubeck/2019/02/07/rust-a-unique-perspective.html ↩︎