Rust 并发安全相关的几个概念(下)
引言
本文介绍 Rust 并发安全相关的几个概念:Send、Sync、Arc,Mutex、RwLock 等之间的联系。这是其中的下篇,主要介绍 Arc,Mutex、RwLock 这几个线程安全相关的类型。
在上一节[1]中,讲解了 Send 和 Sync 这两个线程安全相关的 trait,在此基础上展开其它相关类型的讲解。
Rc
Rc 是 Reference Counted(引用计数)的简写,在 Rust 中,这个数据结构用于实现单线程安全的对指针的引用计数。之所以这个数据结构只是单线程安全,是因为在定义中显式声明了并不实现 Send 和 Sync 这两个 trait:
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> !marker::Send for Rc<T> {}
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> !marker::Sync for Rc<T> {}
个中原因,是因为 Rc 内部的实现中,使用了非原子的引用计数(non-atomic reference counting),因此就不能满足线程安全的条件了。如果要在多线程中使用引用计数,就要使用 Arc 这个类型:
Arc
与 Rc 不同的是,Arc 内部使用了原子操作来实现其引用计数,因此 Arc 是Atomically Reference Counted(原子引用计数)的简写,能被使用在多线程环境中,缺陷是原子操作的性能消耗会更大一些。虽然 Arc 能被用在多线程环境中,并不意味着 Arc
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {}
从声明可以看出:一个 Arc
#![feature(negative_impls)]
use std::sync::Arc;
#[derive(Debug)]
struct Foo {}
impl !Send for Foo {}
fn main() {
let foo = Arc::new(Foo {});
std::thread::spawn(move || {
dbg!(foo);
});
}
在以上的代码中,由于在第 8 行显示声明了 Foo 这个类型不满足 Sync,所以这段代码编译不过,报错信息如下:
= help: the trait `Sync` is not implemented for `Foo`
= note: required because of the requirements on the impl of `Send` for `Arc<Foo>`
反之,如果把第 8 行去掉,代码就能编译通过了。于是,这就带来一个问题:Arc 虽然能被用在多线程环境中,但并不是所有Arc
Mutex
与其它语言不同的是,Rust 中类似 Mutex、RwLock 这样的结构都有一个包裹类型,这带来一个好处:使用这些数据类型保护对一个数据的访问时,是能够明确知道保护的哪个数据的。比如在 C 语言中,可能只是看到一个简单的 mutex 定义:
// 仅看到这个定义,并不知道这个 mutex 保护哪个数据
mutex_t mutex;
但是在 Rust中,定义一个 Mutex 是必须知道保护什么类型的哪个数据的:
let foo = Arc::new(Mutex::new(Foo {}));
这无疑给阅读代码带来了便利。回到线程安全这个话题来,Mutex 只要求包裹的类型 T 满足 Send 就可以将它转成满足 Send 和 Sync 的类型 Mutex
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Send for Mutex<T> {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<T: ?Sized + Send> Sync for Mutex<T> {}
这意味着:即便一个类型只满足了 Send,不能直接用于 Arc
#![feature(negative_impls)]
use std::sync::{Arc, Mutex};
#[derive(Debug)]
struct Foo {}
impl !Sync for Foo {}
fn main() {
let foo = Arc::new(Mutex::new(Foo {}));
std::thread::spawn(move || println!("{:?}", foo));
}
上面这段代码中,Foo 类型声明不满足 Sync,所以不能直接声明 Arc
RwLock
讲解了 Mutex,来看看 RwLock 的使用,顾名思义:RwLock 提供了读写锁的实现。它的 Send 和 Sync 要求如下:
impl<T: ?Sized + Send> Send for RwLock<T>
impl<T: ?Sized + Send + Sync> Sync for RwLock<T>
对比可以看到:RwLock
- RwLock:由于要求内部的类型 T 必须满足 Sync,于是在多个线程中通过 RwLock
同时访问 & T 是安全的。 - Mutex:当 Mutex 对内部的数据进行加锁操作时,相当于将内部的数据发送到了加锁成功的线程上,而解锁时又会将内部数据发送到另一个线程上,于是 Mutex
就仅要求 T 满足 Send 即可。
Because of those bounds, RwLock requires its contents to be Sync, i.e. it's safe for two threads to have a &ptr to that type at the same time. Mutex only requires the data to be Send, because conceptually you can think of it like when you lock the Mutex it sends the data to your thread, and when you unlock it the data gets sent to another thread.
(见:Mutex vs RwLock : rust[2])Interior Mutability
Interior Mutability
Mutex 和 RwLock 的作用,除了将类型 T 包裹起来,提供对该类型数据的多线程安全访问之外,还有一个大的用处:Interior mutability。在 Rust 中,如果传入类型方法的 Self 引用不是 mut 类型的话,是无法对该对象的成员就行修改的,比如:
#[derive(Debug)]
struct Foo {
pub a: u32,
}
fn main() {
let foo = Foo { a: 0 };
foo.a = 1;
}
这段代码无法编译通过,因为 foo 类型为 Foo,因此无法修改其成员,编译器提醒说可以通过把变量 foo 变成可变类型来解决:
error[E0594]: cannot assign to `foo.a`, as `foo` is not declared as mutable
--> src/main.rs:8:5
|
7 | let foo = Foo { a: 0 };
| --- help: consider changing this to be mutable: `mut foo`
8 | foo.a = 1;
| ^^^^^^^^^ cannot assign
但是,如果将内部的成员 a 使用 Mutex 重新包装,即便 foo 仍然不是 mut 类型,也可以进行修改了:
use std::sync::Mutex;
#[derive(Debug)]
struct Foo {
pub a: Mutex<u32>,
}
fn main() {
let foo = Foo { a: Mutex::new(0) };
let mut a = foo.a.lock().unwrap();
*a = 1;
}
这个特点,被称为内部可变性(Interior mutability),这是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据。
总结
- Send 和 Sync 是线程安全类型定义时的两类 marker trait,提供给编译器检查之用。
- 除非显式声明不满足这两个 trait,否则类型都是默认满足这两个 trait 的。
- 一个类型要满足这两类 trait,当且仅当该类型内部的所有成员都满足,编译器在编译时会进行检查。
- Rc 只能提供引用计数功能,并不能在多线程环境下使用;反之,Arc 内部使用原子变量实现了引用计数,因此可以在多线程环境下使用。
- 一个类型 T 如果只满足 Send,可以通过 Mutex 包裹成 Mutex
类型来满足多线程安全;但是 RwLock 要求比 Mutex 更严格。 - 除了多线程安全之外,Mutex 和 RwLock 等类型还提供了内部可变性(Interior mutability)这个作用。
参考资料
- Arc and Mutex in Rust | It's all about the bit[3]
- Sync in std::marker - Rust[4]
- Send in std::marker - Rust[5]
- Send and Sync - The Rustonomicon[6]
- rust - Understanding the Send trait - Stack Overflow[7]
- Understanding Rust Thread Safety[8]
- An unsafe tour of Rust’s Send and Sync | nyanpasu64’s blog[9]
- Rust: A unique perspective[10]
- std::rc - Rust[11]
- Arc in std::sync - Rust[12]
- Mutex in std::sync - Rust[13]
- RwLock in std::sync - Rust[14]
- multithreading - When or why should I use a Mutex over an RwLock? - Stack Overflow[15]
关于 Databend
Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式数仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。
-
Databend 文档:https://databend.rs/
-
Wechat:Databend
https://www.reddit.com/r/rust/comments/5bx34b/mutex_vs_rwlock ↩︎
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 ↩︎
https://doc.rust-lang.org/std/sync/struct.RwLock.html#impl-Send ↩︎
https://stackoverflow.com/questions/50704279/when-or-why-should-i-use-a-mutex-over-an-rwlock ↩︎