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 !marker::Send for Rc {}

#[stable(feature = "rust1", since = "1.0.0")]
impl !marker::Sync for Rc {}

个中原因,是因为 Rc 内部的实现中,使用了非原子的引用计数(non-atomic reference counting),因此就不能满足线程安全的条件了。如果要在多线程中使用引用计数,就要使用 Arc 这个类型:

Arc

与 Rc 不同的是,Arc 内部使用了原子操作来实现其引用计数,因此 Arc 是Atomically Reference Counted(原子引用计数)的简写,能被使用在多线程环境中,缺陷是原子操作的性能消耗会更大一些。虽然 Arc 能被用在多线程环境中,并不意味着 Arc 天然就实现了 Send 和 Sync,来看看这两部分的声明:

#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl Send for Arc {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl Sync for Arc {}

从声明可以看出:一个 Arc 类型,当且仅当包裹(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`

反之,如果把第 8 行去掉,代码就能编译通过了。于是,这就带来一个问题:Arc 虽然能被用在多线程环境中,但并不是所有Arc 都是线程安全的,如果里面包裹的类型 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

#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl Send for Mutex {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl Sync for Mutex {}

这意味着:即便一个类型只满足了 Send,不能直接用于 Arc 满足多线程安全,但是可以通过包装成 Mutex 来达到线程安全的目的。来看看上面的代码如何使用 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 外面再包一层 Mutex,变成 Arc> 这样就能在多线程中使用了。即:一个只需要满足 Send 要求的类型 T,只要经过 Mutex 的包裹变成类型 Mutex,就变成了一个线程安全的类型。个中原因:Mutex 只要求类型 T 满足 Send 即可,内部的机制会保证这个类型在多线程环境下安全访问。RwLock

RwLock

讲解了 Mutex,来看看 RwLock 的使用,顾名思义:RwLock 提供了读写锁的实现。它的 Send 和 Sync 要求如下:

impl Send for RwLock
impl Sync for RwLock

对比可以看到:RwLock 要满足 Sync,要求类型 T 同时满足 Send 和Sync,这个条件是比 Mutex 更强的条件。也可以这么来理解 RwLock 和 Mutex 的区别:

  • 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 : 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,
}
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 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。


文章首发于公众号:Databned

你可能感兴趣的:(rust)