在并发的世界中,最常见的并发安全问题就是数据竞争,也就是两个线程同时对一个变量进行读写操作。但当你在 safe rust 中写出有数据竞争的代码时,编译器会直接拒绝编译。那么它是靠什么魔法做到的呢?
这就不得不谈 send 和 sync 这两个标记 trait 了,实现 send 的类型可以在多线程间转移所有权,实现 sync 的类型可以在多线程间共享引用。但它们内部都是没有任何方法声明以及方法体的,二者仅仅是作为一个类型约束的标记信息提供给编译器,帮助编译器拒绝线程不安全的代码。
定义:
pub unsafe auto trait send { }
pub unsafe auto trait sync { }
本文将深入探讨 sync
和 send
traits,了解为什么某些类型实现这些 traits,而另一些则没有,并讨论 rust 中并发编程的最佳实践。
the sync trait
sync
trait 表示一个类型可以安全地被多个线程同时访问。这里的访问指的是只读共享安全。rust 中几乎所有的原始类型都实现了 sync
trait
例如:
let x = 5; // i32 is sync
i32
类型实现了 sync
,所以在线程间共享 i32
值是安全的。
另一方面,提供内部可变性的类型(内部可变性指的是在拥有不可变引用的时候,依然可以获取到其内部成员的可变引用,进而对其数据进行修改。),如 mutex
,其中 t 未实现 sync
trait。
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl send for mutex {}
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl sync for mutex {}
因为 mutex
使用锁来保护对内部数据的访问,如果多个线程同时访问它,可能会导致数据竞争或死锁。
举例来说:
use std::sync::mutex;
let m = mutex::new(5); //mutex is not sync
mutex
类型没有实现 sync
,所以跨线程共享是不安全的。
为在多个线程安全地访问非 sync
类型(如 mutex
),我们必须使用适当的同步操作,如获取锁,执行操作和释放锁,在本文后面看到使用互斥锁和其他线程安全类型的示例。
支持 sync 的类型
rust 中的 sync trait 确保了对同一数据的多个引用(无论是可变的还是不可变的)可以安全地从多个线程并发访问。任何实现 sync trait 的类型 t
都可以被认为是“线程安全”的。
rust 中的 sync 类型的一些例子是:
- 原始类型,如
i32
、bool
、char
等。 - 简单的聚合类型,如元组
(i32, bool)
- 原子类型,如
atomicbool
另一方面,非同步类型不能同时使用多个引用,因为这可能导致数据竞争。非同步类型的一些示例包括:
mutex
- 在访问内部 i32 之前需要锁定互斥体。refcell
- 在访问内部值之前需要借用 refcell。rc
- 共享了内部 i32 的所有权,所以多个可变借用是不安全的。
非 sync 类型多线程访问
mutex
为在多个线程安全地访问非同步类型,我们需要使用同步原语,如互斥锁。若仅仅使用 mutex 而不使用 arc ,可使用像作用域线程(crossbeam),例如:
这里,我们使用 mutex
来安全地从多个线程中修改和读取内部 string。 lock()
方法获取锁,阻止其他线程访问互斥体。
atomic
像 atomicu64
这样的原子类型也可以使用像 fetch_add()
这样的原子操作从多个线程安全地访问。例如:
总结
因此,总而言之,要在 rust 中跨线程共享数据,数据必须:
- 类型为
sync
(原始/不可变类型) - 封装在互斥或原子类型中(mutex、rwlock、atomic*)
- 使用像通道这样的消息传递技术来跨线程传递数据的所有权。
the send trait
rust 中的 send
trait 表示类型可以安全地跨线程边界传输。如果一个类型实现了 send
,这意味着该类型的值的所有权可以在线程之间转移。
例如,像 i32
和 bool
这样的原始类型是 send
,
因为它们在线程之间共享时没有任何内部引用或可变而导致问题:
然而,像 rc
这样的类型未实现 send
,因为它的引用计数在内部发生了变化,并且多个线程改变相同的引用计数可能会导致内存不安全:
像 rc
这样的非 send
类型不能跨线程传输,但它们仍然可以在单个线程中使用。当线程需要共享一些数据时,非 send
类型可以被包装在像 arc
这样的线程安全的包装器中,arc
总结一下,关于 send
的几个关键点是:
- 类型
send
可以在线程之间转移所有权 - 像
i32
和bool
这样的原始类型是send
- 具有内部可变的类型(如
rc
)通常不是send
- 非
send
类型仍然可以在单个线程中使用,或者在包装在像arc
这样的线程安全的容器中时在线程之间共享 - 跨线程传输非
send
类型会导致未定义的行为和内存不安全
自定义实现 sync 和 send
要创建自定义类型 sync
或 send
,您只需实现类型的 sync
和 send
trait。
这里有一个 持有裸指针*const u8
的 mybox
结构体, 由于只要复合类型中有一个成员不是 send 或者 sync,那么该类型也就不是 send 或 sync。裸指针*const u8
均未实现 send
和 sync trait
故 mybox
复合类型也不是 send
或 sync
。
若给 mybox 实现了 send 和 sync 则借助 arc 可在线程间传递和共享数据。当然建议自己不要轻易去实现 sync 和 send trait ,一旦实现就要为被实现类型的线程安全性负责。这件事本来就是一件很难保证的事情。
有些类型是不可能生成sync
和send
的,因为它们包含非sync
/非send
类型或允许多线程的可变。例如, rc
不能被设置为send
,因为引用计数需要被原子地更新,而refcell
不能被设置为 sync
,因为它的借用检查不是线程安全的。
同步/发送规则和最佳实践
重要的是要记住混合sync
/send
和非sync
/非send
类型的规则。一些需要遵守的关键规则:
类型必须是send
才能在线程之间移动。这意味着像rc
这样的类型不能跨线程共享,因为它们不是send
。
- 如果一个类型包含一个非
send
类型,那么外部类型不能是send
。例如option
不是> send
,因为rc
不是send
。 sync
类型可以通过共享引用从多个线程并发使用。非sync
类型不能同时使用它们的值,并且一次只能在一个线程中可变。- 如果一个类型包含一个非
sync
类型,那么外部类型不能是sync
。例如mutex
不是> sync
,因为rc
不是sync
。
并发 rust 代码的一些最佳实践:
- 尽可能避免可变。支持不可变的数据结构和逻辑。
- 当需要修改时,使用同步原语(如
mutex
)来安全地从多个线程进行。 - 使用消息传递在线程之间进行通信,而不是直接共享内存。这有助于避免数据竞争和未定义的行为。
- 尽可能地限制为修改锁定数据的范围。持有锁太长时间会影响性能和吞吐量。
- 根据它们是否实现
sync
和send
仔细选择类型。例如,在线程之间共享时,首选arc
而不是rc
。 - 使用
atomic
类型进行简单的并发访问原语类型。它们允许从多个线程访问而不加锁。
[基于 send 和 sync 的线程安全 - rust 语言圣经(rust course)]( "基于 send 和 sync 的线程安全 - rust 语言圣经(rust course "基于 send 和 sync 的线程安全 - rust 语言圣经(rust course)")")