Send 和 Sync
然而,并非所有事物都遵循继承的可变性。某些类型允许您在内存中的某个位置拥有多个别名,同时对其进行修改。除非这些类型使用同步来管理此访问,否则它们绝对不是线程安全的。Rust 通过 Send
和 Sync
trait 捕获了这一点。
- 如果将一个类型发送到另一个线程是安全的,则该类型是 Send 的。
- 如果一个类型在线程之间共享是安全的(当且仅当
&T
是 Send 时,T 是 Sync),则该类型是 Sync 的。
Send 和 Sync 对于 Rust 的并发故事至关重要。因此,存在大量的特殊工具来使它们正确工作。首先,它们是 unsafe trait。这意味着它们实现起来是不安全的,而其他 unsafe 代码可以假设它们被正确实现。由于它们是标记 trait(它们没有像方法这样的关联项),因此正确实现仅仅意味着它们具有实现者应具有的内在属性。错误地实现 Send 或 Sync 可能会导致未定义行为。
Send 和 Sync 也是自动派生的 trait。这意味着,与所有其他 trait 不同,如果一个类型完全由 Send 或 Sync 类型组成,那么它就是 Send 或 Sync。几乎所有基本类型都是 Send 和 Sync,因此几乎所有您会与之交互的类型都是 Send 和 Sync。
主要例外包括
- 裸指针既不是 Send 也不是 Sync(因为它们没有安全保护)。
UnsafeCell
不是 Sync 的(因此Cell
和RefCell
也不是)。Rc
不是 Send 或 Sync 的(因为引用计数是共享且未同步的)。
Rc
和 UnsafeCell
从根本上来说不是线程安全的:它们启用了未同步的共享可变状态。然而,严格来说,裸指针被标记为线程不安全更像是一种lint。对裸指针做任何有用的事情都需要解引用它,这本身就是不安全的。从这个意义上讲,有人可能会认为将它们标记为线程安全“也行”。
然而,重要的是它们不是线程安全的,以防止包含它们的类型被自动标记为线程安全。这些类型具有重要的未跟踪所有权,并且作者不太可能认真考虑过线程安全性。在 Rc
的情况下,我们有一个很好的例子,说明一个类型包含一个绝对不是线程安全的 *mut
。
未自动派生的类型如果需要可以简单地实现它们
#![allow(unused)] fn main() { struct MyBox(*mut u8); unsafe impl Send for MyBox {} unsafe impl Sync for MyBox {} }
在极其罕见的情况下,如果一个类型被不恰当地自动派生成为 Send 或 Sync,那么也可以取消实现 Send 和 Sync
#![allow(unused)] #![feature(negative_impls)] fn main() { // I have some magic semantics for some synchronization primitive! struct SpecialThreadToken(u8); impl !Send for SpecialThreadToken {} impl !Sync for SpecialThreadToken {} }
请注意,就其本身而言,不可能错误地派生 Send 和 Sync。只有被其他 unsafe 代码赋予特殊含义的类型才可能因错误地成为 Send 或 Sync 而引起麻烦。
裸指针的大多数用法应该封装在足够的抽象之后,以便可以派生 Send 和 Sync。例如,尽管 Rust 的所有标准集合都广泛使用裸指针来管理分配和复杂的所有权,但它们都是 Send 和 Sync 的(当它们包含 Send 和 Sync 类型时)。同样,这些集合的大多数迭代器都是 Send 和 Sync 的,因为它们在很大程度上表现得像集合的 &
或 &mut
。
示例
Box
由编译器实现为它自己的特殊内在类型,出于 各种原因,但我们可以自己实现一些行为类似的东西,以查看何时实现 Send 和 Sync 是合理的示例。我们称之为 Carton
。
我们首先编写代码,将分配在栈上的值转移到堆上。
#![allow(unused)] fn main() { pub mod libc { pub use ::std::os::raw::{c_int, c_void}; #[allow(non_camel_case_types)] pub type size_t = usize; extern "C" { pub fn posix_memalign(memptr: *mut *mut c_void, align: size_t, size: size_t) -> c_int; } } use std::{ mem::{align_of, size_of}, ptr, cmp::max, }; struct Carton<T>(ptr::NonNull<T>); impl<T> Carton<T> { pub fn new(value: T) -> Self { // Allocate enough memory on the heap to store one T. assert_ne!(size_of::<T>(), 0, "Zero-sized types are out of the scope of this example"); let mut memptr: *mut T = ptr::null_mut(); unsafe { let ret = libc::posix_memalign( (&mut memptr as *mut *mut T).cast(), max(align_of::<T>(), size_of::<usize>()), size_of::<T>() ); assert_eq!(ret, 0, "Failed to allocate or invalid alignment"); }; // NonNull is just a wrapper that enforces that the pointer isn't null. let ptr = { // Safety: memptr is dereferenceable because we created it from a // reference and have exclusive access. ptr::NonNull::new(memptr) .expect("Guaranteed non-null if posix_memalign returns 0") }; // Move value from the stack to the location we allocated on the heap. unsafe { // Safety: If non-null, posix_memalign gives us a ptr that is valid // for writes and properly aligned. ptr.as_ptr().write(value); } Self(ptr) } } }
这不是很有用,因为一旦我们的用户给我们一个值,他们就无法访问它。Box
实现了 Deref
和 DerefMut
,以便您可以访问内部值。让我们这样做。
#![allow(unused)] fn main() { use std::ops::{Deref, DerefMut}; struct Carton<T>(std::ptr::NonNull<T>); impl<T> Deref for Carton<T> { type Target = T; fn deref(&self) -> &Self::Target { unsafe { // Safety: The pointer is aligned, initialized, and dereferenceable // by the logic in [`Self::new`]. We require readers to borrow the // Carton, and the lifetime of the return value is elided to the // lifetime of the input. This means the borrow checker will // enforce that no one can mutate the contents of the Carton until // the reference returned is dropped. self.0.as_ref() } } } impl<T> DerefMut for Carton<T> { fn deref_mut(&mut self) -> &mut Self::Target { unsafe { // Safety: The pointer is aligned, initialized, and dereferenceable // by the logic in [`Self::new`]. We require writers to mutably // borrow the Carton, and the lifetime of the return value is // elided to the lifetime of the input. This means the borrow // checker will enforce that no one else can access the contents // of the Carton until the mutable reference returned is dropped. self.0.as_mut() } } } }
最后,让我们考虑一下我们的 Carton
是否是 Send 和 Sync。除非某个事物在不强制执行独占访问的情况下与其他事物共享可变状态,否则它可以安全地成为 Send。每个 Carton
都有一个唯一的指针,所以我们没问题。
#![allow(unused)] fn main() { struct Carton<T>(std::ptr::NonNull<T>); // Safety: No one besides us has the raw pointer, so we can safely transfer the // Carton to another thread if T can be safely transferred. unsafe impl<T> Send for Carton<T> where T: Send {} }
Sync 呢?为了使 Carton
成为 Sync,我们必须确保您不能在写入存储在 &Carton
中的内容时,同时从另一个 &Carton
读取或写入相同的内容。由于您需要 &mut Carton
才能写入指针,并且借用检查器强制执行可变引用必须是独占的,因此使 Carton
成为 sync 也没有健全性问题。
#![allow(unused)] fn main() { struct Carton<T>(std::ptr::NonNull<T>); // Safety: Since there exists a public way to go from a `&Carton<T>` to a `&T` // in an unsynchronized fashion (such as `Deref`), then `Carton<T>` can't be // `Sync` if `T` isn't. // Conversely, `Carton` itself does not use any interior mutability whatsoever: // all the mutations are performed through an exclusive reference (`&mut`). This // means it suffices that `T` be `Sync` for `Carton<T>` to be `Sync`: unsafe impl<T> Sync for Carton<T> where T: Sync {} }
当我们断言我们的类型是 Send 和 Sync 时,我们通常需要确保每个包含的类型都是 Send 和 Sync。在编写行为类似于标准库类型的自定义类型时,我们可以断言我们具有相同的要求。例如,以下代码断言,如果同类型的 Box 是 Send 的,则 Carton 是 Send 的,在这种情况下,这与说 T 是 Send 的相同。
#![allow(unused)] fn main() { struct Carton<T>(std::ptr::NonNull<T>); unsafe impl<T> Send for Carton<T> where Box<T>: Send {} }
目前 Carton<T>
存在内存泄漏,因为它从不释放它分配的内存。一旦我们修复了这个问题,我们就有一个新的要求,我们必须确保满足才能成为 Send:我们需要知道 free
可以在一个指针上调用,该指针是由在另一个线程上完成的分配产生的。我们可以在 libc::free
的文档中检查这是否为真。
#![allow(unused)] fn main() { struct Carton<T>(std::ptr::NonNull<T>); mod libc { pub use ::std::os::raw::c_void; extern "C" { pub fn free(p: *mut c_void); } } impl<T> Drop for Carton<T> { fn drop(&mut self) { unsafe { libc::free(self.0.as_ptr().cast()); } } } }
MutexGuard 是一个很好的例子,说明这种情况不会发生:请注意 它不是 Send 的。MutexGuard 的实现 使用了库,这些库要求您确保不要尝试释放在不同线程中获取的锁。如果您能够将 MutexGuard 发送到另一个线程,则析构函数将在您发送它的线程中运行,从而违反了该要求。MutexGuard 仍然可以是 Sync 的,因为您可以发送到另一个线程的只是 &MutexGuard
,而删除引用不会做任何事情。
待办事项:更好地解释什么可以是或不可以是 Send 或 Sync。仅对数据竞争提出申诉是否足够?