周刊(第20期):Rust并发安全相关的几个概念(下)

2022-06-25
6分钟阅读时长

引言:本文介绍Rust并发安全相关的几个概念:SendSyncArcMutexRwLock等之间的联系。这是其中的下篇,主要介绍ArcMutexRwLock这几个线程安全相关的类型。


Rust并发安全相关的几个概念(下)

上一节中,讲解了SendSync这两个线程安全相关的trait,在此基础上展开其它相关类型的讲解。

Rc

RcReference Counted(引用计数)的简写,在Rust中,这个数据结构用于实现单线程安全的对指针的引用计数。之所以这个数据结构只是单线程安全,是因为在定义中显式声明了并不实现SendSync这两个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内部使用了原子操作来实现其引用计数,因此ArcAtomically Reference Counted(原子引用计数)的简写,能被使用在多线程环境中,缺陷是原子操作的性能消耗会更大一些。

虽然Arc能被用在多线程环境中,并不意味着Arc<T>天然就实现了SendSync,来看看这两部分的声明:

#[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满足SyncSend时才能被认为是满足SendSync的类型。来做一个实验:

#![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中类似MutexRwLock这样的结构都有一个包裹类型,这带来一个好处:使用这些数据类型保护对一个数据的访问时,是能够明确知道保护的哪个数据的。比如在C语言中,可能只是看到一个简单的mutex定义:

// 仅看到这个定义,并不知道这个mutex保护哪个数据
mutex_t mutex;

但是在Rust中,定义一个Mutex是必须知道保护什么类型的哪个数据的:

let foo = Arc::new(Mutex::new(Foo {}));

这无疑给阅读代码带来了便利。

回到线程安全这个话题来,Mutex只要求包裹的类型T满足Send就可以将它转成满足SendSync的类型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提供了读写锁的实现。它的SendSync要求如下:

impl<T: ?Sized + Send> Send for RwLock<T>
impl<T: ?Sized + Send + Sync> Sync for RwLock<T>

对比可以看到:RwLock<T>要满足Sync,要求类型T同时满足SendSync,这个条件是比Mutex<T>更强的条件。

也可以这么来理解RwLockMutex的区别:

  • 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 : rust

Interior Mutability

MutexRwLock的作用,除了将类型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 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据。

总结

  • SendSync是线程安全类型定义时的两类marker trait,提供给编译器检查之用。
  • 除非显式声明不满足这两个trait,否则类型都是默认满足这两个trait的。
  • 一个类型要满足这两类trait,当且仅当该类型内部的所有成员都满足,编译器在编译时会进行检查。
  • Rc只能提供引用计数功能,并不能在多线程环境下使用;反之,Arc内部使用原子变量实现了引用计数,因此可以在多线程环境下使用。
  • 一个类型T如果只满足Send,可以通过Mutex包裹成Mutex<T>类型来满足多线程安全;但是RwLock要求比Mutex更严格。
  • 除了多线程安全之外,MutexRwLock等类型还提供了内部可变性(Interior mutability)这个作用。

参考资料