共计 5567 个字符,预计需要花费 14 分钟才能阅读完成。
引言
本文介绍 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<T> 人造就实现了 Send 和 Sync,来看看这两局部的申明:
#[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<T> 类型,当且仅当包裹(wrap)的类型 T 满足 Sync 和 Send 时能力被认为是满足 Send 和 Sync 的类型。来做一个试验:
#![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<T> 都是线程平安的,如果外面包裹的类型 T 并不满足多线程平安,是不是就不能应用了?解开这个问题的答案请应用 Mutex 类型:Mutex
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<T>:
#[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<T> 满足多线程平安,然而能够通过包装成 Mutex<T> 来达到线程平安的目标。来看看下面的代码如何应用 Mutex 来进行革新:
#![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<Foo> 用在多线程环境中,这一点下面的试验曾经证实。然而,能够在 Foo 里面再包一层 Mutex,变成 Arc<Mutex<Foo>> 这样就能在多线程中应用了。即:一个只须要满足 Send 要求的类型 T,只有通过 Mutex 的包裹变成类型 Mutex<T>,就变成了一个线程平安的类型。个中起因:Mutex 只要求类型 T 满足 Send 即可,外部的机制会保障这个类型在多线程环境下平安拜访。RwLock
RwLock
解说了 Mutex,来看看 RwLock 的应用,顾名思义:RwLock 提供了读写锁的实现。它的 Send 和 Sync 要求如下:
impl<T: ?Sized + Send> Send for RwLock<T>
impl<T: ?Sized + Send + Sync> Sync for RwLock<T>
比照能够看到:RwLock<T> 要满足 Sync,要求类型 T 同时满足 Send 和 Sync,这个条件是比 Mutex<T> 更强的条件。也能够这么来了解 RwLock 和 Mutex 的区别:
- RwLock:因为要求外部的类型 T 必须满足 Sync,于是在多个线程中通过 RwLock<T> 同时拜访 & T 是平安的。
- Mutex:当 Mutex 对外部的数据进行加锁操作时,相当于将外部的数据发送到了加锁胜利的线程上,而解锁时又会将外部数据发送到另一个线程上,于是 Mutex<T> 就仅要求 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 : rust2)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<T> 类型来满足多线程平安;然而 RwLock 要求比 Mutex 更严格。
- 除了多线程平安之外,Mutex 和 RwLock 等类型还提供了外部可变性(Interior mutability)这个作用。
参考资料
- Arc and Mutex in Rust | It’s all about the bit3
- Sync in std::marker – Rust4
- Send in std::marker – Rust5
- Send and Sync – The Rustonomicon6
- rust – Understanding the Send trait – Stack Overflow7
- Understanding Rust Thread Safety8
- An unsafe tour of Rust’s Send and Sync | nyanpasu64’s blog9
- Rust: A unique perspective10
- std::rc – Rust11
- Arc in std::sync – Rust12
- Mutex in std::sync – Rust13
- RwLock in std::sync – Rust14
- multithreading – When or why should I use a Mutex over an RwLock? – Stack Overflow15
对于 Databend
Databend 是一款开源、弹性、低成本,基于对象存储也能够做实时剖析的旧式数仓。期待您的关注,一起摸索云原生数仓解决方案,打造新一代开源 Data Cloud。
- Databend 文档:https://databend.rs/
- Twitter:https://twitter.com/Datafuse_…
- Slack:https://datafusecloud.slack.com/
- Wechat:Databend
- GitHub:https://github.com/datafusela…
文章首发于公众号:Databned
- https://www.codedump.info/pos… ↩
- https://www.reddit.com/r/rust… ↩
- https://itsallaboutthebit.com… ↩
- https://doc.rust-lang.org/std… ↩
- https://doc.rust-lang.org/std… ↩
- https://doc.rust-lang.org/nom… ↩
- https://stackoverflow.com/que… ↩
- https://onesignal.com/blog/th… ↩
- https://nyanpasu64.github.io/… ↩
- https://limpet.net/mbrubeck/2… ↩
- https://doc.rust-lang.org/std… ↩
- https://doc.rust-lang.org/std… ↩
- https://doc.rust-lang.org/std… ↩
- https://doc.rust-lang.org/std… ↩
- https://stackoverflow.com/que… ↩