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<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 : 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<T> 类型来满足多线程安全;但是 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]

  1. https://www.codedump.info/post/20220619-weekly-19/

  2. https://www.reddit.com/r/rust/comments/5bx34b/mutex_vs_rwlock

  3. https://itsallaboutthebit.com/arc-mutex

  4. https://doc.rust-lang.org/std/marker/trait.Sync.html

  5. https://doc.rust-lang.org/std/marker/trait.Send.html

  6. https://doc.rust-lang.org/nomicon/send-and-sync.html

  7. https://stackoverflow.com/questions/59428096/understanding-the-send-trait

  8. https://onesignal.com/blog/thread-safety-rust

  9. https://nyanpasu64.github.io/blog/an-unsafe-tour-of-rust-s-send-and-sync/#example-passing-mut-t-send-between-threads

  10. https://limpet.net/mbrubeck/2019/02/07/rust-a-unique-perspective.html

  11. https://doc.rust-lang.org/std/rc/index.html

  12. https://doc.rust-lang.org/std/sync/struct.Arc.html

  13. https://doc.rust-lang.org/std/sync/struct.Mutex.html

  14. https://doc.rust-lang.org/std/sync/struct.RwLock.html#impl-Send

  15. https://stackoverflow.com/questions/50704279/when-or-why-should-i-use-a-mutex-over-an-rwlock

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容