发送与同步
不过,并非所有东西都遵循继承的可变性。某些类型允许您在内存中某个位置拥有多个别名,同时对其进行修改。除非这些类型使用同步来管理此访问,否则它们绝对不是线程安全的。Rust 通过 Send
和 Sync
trait 来捕获这一点。
- 如果可以安全地将某个类型发送到另一个线程,则该类型为 Send。
- 如果某个类型可以安全地在多个线程之间共享,则该类型为 Sync(当且仅当
&T
为 Send 时,T 才为 Sync)。
Send 和 Sync 是 Rust 并发故事的基础。因此,存在大量特殊工具来确保它们正常工作。首先,它们是 非安全 trait。这意味着实现它们是不安全的,而其他非安全代码可以假设它们已正确实现。由于它们是*标记 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
从根本上讲不是线程安全的:它们启用了未同步的共享可变状态。然而,严格来说,将原始指针标记为线程不安全更像是一种*提示*。使用原始指针执行任何有用的操作都需要对其进行解引用,这本身就是不安全的。从这个意义上说,可以认为将它们标记为线程安全是“可以的”。
然而,重要的是,它们不是线程安全的,以防止包含它们的类型被自动标记为线程安全。这些类型具有非平凡的未跟踪所有权,并且它们的作者不太可能认真考虑线程安全问题。对于 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。只有那些被其他非安全代码赋予特殊含义的类型才有可能因为错误地成为 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
,而丢弃引用不会执行任何操作。
TODO:更好地解释什么可以或不能是 Send 或 Sync。仅诉诸数据竞争就足够了吗?