周刊(第20期):Rust并发安全相关的几个概念(下)
引言:本文介绍Rust并发安全相关的几个概念:Send
、Sync
、Arc
,Mutex
、RwLock
等之间的联系。这是其中的下篇,主要介绍Arc
,Mutex
、RwLock
这几个线程安全相关的类型。
Rust并发安全相关的几个概念(下)
在上一节中,讲解了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
与其它语言不同的是,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
讲解了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.
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 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
- std::rc - Rust
- Arc in std::sync - Rust
- Mutex in std::sync - Rust
- RwLock in std::sync - Rust
- multithreading - When or why should I use a Mutex over an RwLock? - Stack Overflow